Files
zyt/server/tests/TcmTaskAnalysisContentR37Test.php
T
2026-10-27 14:03:35 +08:00

200 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
/** Offline public-endpoint integration. No network, database, provider or patient data. */
namespace think\facade {
class Log {
public static function warning(string $message, array $context = []): void {}
public static function write(string $message, string $level = ''): void {}
}
}
namespace app\common\cache {
class AdminAuthCache {
public function __construct(int $id) {}
public function getAdminUri(): array {
return ['tcm.diagnosis/aiassistant', 'tcm.diagnosis/aianalysis', 'tcm.diagnosis/aigenerateprescription'];
}
}
}
namespace app\adminapi\logic\firstvisit {
class MyPatientLogic {
public static function canAccessDiagnosis($id, $adminId, $adminInfo): bool { return true; }
}
}
namespace app\adminapi\logic\tcm {
function config(string $key): array {
return ['timeout' => 90, 'models' => [
'qwen' => ['name' => 'r37-local-qwen', 'label' => 'R37 local Qwen'],
'openai' => ['name' => 'r37-local-openai', 'label' => 'R37 local OpenAI'],
]];
}
class DiagnosisLogic {
public static function detail($params, $adminInfo): array { return $GLOBALS['r37Current']; }
}
class PatientAiReportLogic {
public static function contextForAuthorizedDiagnosis($diagnosis, $adminId, $adminInfo): array {
return $GLOBALS['r37Context'];
}
public static function compactSourceForPrompt(...$args): array {
throw new \RuntimeException('Unexpected compaction; finite source is below the existing budget.');
}
}
}
namespace app\common\service {
class DifyChatService {
public static array $calls = [];
public static array $responses = [];
public static int $totalCalls = 0;
public static bool $streaming = false;
private static function take(): array {
if (self::$responses === []) throw new \RuntimeException('Unexpected extra local transport invocation.');
return array_shift(self::$responses);
}
public static function chat($profile, $inputs, $query, $user, $files = [], $options = []): array {
++self::$totalCalls;
self::$calls[] = compact('profile', 'inputs', 'query', 'user', 'files', 'options') + ['transport' => 'chat'];
return self::take();
}
public static function streamChat($profile, $inputs, $query, $user, $delta, $abort, $files = [], $options = []): array {
++self::$totalCalls;
self::$calls[] = compact('profile', 'inputs', 'query', 'user', 'files', 'options') + ['transport' => 'stream'];
$result = self::take();
if (empty($result['ok'])) return $result;
self::$streaming = true;
try {
foreach (mb_str_split($result['content'], 19, 'UTF-8') as $chunk) {
if (($abort !== null && $abort()) || $delta($chunk) === false) {
return ['ok' => false, 'error_code' => 'AI_ASSISTANT_CANCELLED'];
}
}
} finally { self::$streaming = false; }
return $result;
}
}
}
namespace {
require dirname(__DIR__) . '/vendor/autoload.php';
use app\adminapi\logic\tcm\DiagnosisAiLogic;
use app\adminapi\service\AiRequiredSource;
use app\adminapi\service\TcmEvidenceGuard;
use app\adminapi\service\TcmClinicalFactStateGuard;
use app\common\service\DifyChatService;
$checks = [];
function r37Check(bool $passed, string $name): void { $GLOBALS['checks'][] = compact('passed', 'name'); }
function r37Reset(array $responses): void {
DifyChatService::$calls = [];
DifyChatService::$responses = $responses;
}
function r37Fixture(string $variant): void {
$GLOBALS['r37Current'] = [
'id' => 37, 'patient_id' => 999, 'diagnosis_date' => '2026-10-26', 'age' => 46,
'chief_complaint' => $variant === 'tongue_missing' ? '口干两周。' : '未记录',
'tongue' => $variant === 'symptoms_missing' ? '舌淡红,苔薄白。' : '未记录',
'pulse' => '脉细', 'allergy_history' => '不详',
'past_history' => '2020年外院糖尿病诊断记录。',
'current_medications' => '二甲双胍500mg,每日2次。',
'fasting_blood_sugar' => 7.9, 'systolic_pressure' => 132, 'diastolic_pressure' => 82,
];
$sections = []; $metadata = [];
foreach (AiRequiredSource::LABELS as $key => $label) {
$value = [['marker' => 'R37_FULL_SOURCE_' . $key]];
if ($key === 'diagnosis_info') $value[] = $GLOBALS['r37Current'];
if ($key === 'prescriptions') $value[] = ['prescription_date' => '2020-02-01',
'prescription_name' => '历史用药记录', 'usage' => '格列美脲已停用。'];
if ($key === 'daily_tracking') $value[] = ['record_date' => '2026-10-25',
'fasting_blood_sugar' => 8.3, 'note' => '晚餐主食较多,晚饭后步行20分钟。'];
if ($key === 'chat') $value[] = ['msg_time' => '2020-02-01', 'text' => '历史自述口渴,非本次采集。'];
$sections[] = compact('key', 'label', 'value');
$metadata[] = ['key' => $key, 'label' => $label, 'availability' => 'present', 'count' => count($value), 'status' => 'prepared'];
}
$GLOBALS['r37Context'] = [
'snapshot' => ['required_content' => $sections],
'source_summary' => ['required_sections' => $metadata, 'snapshot_complete' => true],
'source_diagnosis_ids' => [12, 37],
'files' => [['type' => 'image', 'url' => '/offline-r37/attachment.png']],
];
}
// Namespace transport/auth/context helpers reused from the independently saved
// source_ids_repro.php. New content-contract expectations below are root-authored.
$checks = []; $responses = [];
$ids = ['f1', 'f1, f2', "f1\nf2", 'f1、f2;f3', '["f1","f2"]',
"```json\n[\"f1\",\"f2\"]\n```", "```text\nf1, f2\n```", "\xEF\xBB\xBFf1 f2"];
foreach ($ids as $text) {
r37Check(TcmEvidenceGuard::isSourceIdListOnly($text), 'closed ID-only shape recognized: '.json_encode($text));
}
r37Fixture('both_missing');
foreach (['case_analysis','glucose_trend','medication_review','complication_risk','summary','lifestyle_advice'] as $task) {
foreach (['ordinary','stream'] as $mode) {
r37Reset([['ok'=>true,'content'=>'f1, f2']]); $visible='';
$p=DiagnosisAiLogic::prepareAssistant(37,$task,'请分析已有资料及其局限。',7,[]);
$result=$mode==='ordinary' ? DiagnosisAiLogic::assistant(37,$task,'请分析已有资料及其局限。',7,[])
: DiagnosisAiLogic::streamPreparedAssistant($p,static function($delta)use(&$visible){$visible.=$delta;return true;});
r37Check($result===null && $visible==='' && DiagnosisAiLogic::getAssistantErrorCode()==='TASK_ANALYSIS_MISSING'
&& count(DifyChatService::$calls)===1, "$task/$mode rejects exact independent counterexample honestly");
}
}
foreach ($ids as $text) foreach (['ordinary','stream'] as $mode) {
r37Reset([['ok'=>true,'content'=>$text]]);$visible='';
$p=DiagnosisAiLogic::prepareAssistant(37,'glucose_trend','请分析已有血糖记录及局限。',7,[]);
$result=$mode==='ordinary' ? DiagnosisAiLogic::assistant(37,'glucose_trend','请分析已有血糖记录及局限。',7,[])
: DiagnosisAiLogic::streamPreparedAssistant($p,static function($delta)use(&$visible){$visible.=$delta;return true;});
r37Check($result===null && $visible==='' && DiagnosisAiLogic::getAssistantErrorCode()==='TASK_ANALYSIS_MISSING',
"$mode ID-only variant rejected before output: ".json_encode($text));
$guard=TcmEvidenceGuard::assess($r37Current,$r37Context['snapshot']);
$review=TcmEvidenceGuard::review($guard,$text);
r37Check(!$review['valid'] && $review['answer']==='' && in_array('TASK_ANALYSIS_MISSING',array_column($review['issues'],'code'),true),
'compatibility review does not manufacture ID-only analysis');
}
foreach (['当前分析引用资料编号f1和f2;既往记录与本次判断保留时点限制。',
'f1, f2仅为引文编号,已有血糖记录的采样条件仍待核实。'] as $text) foreach (['ordinary','stream'] as $mode) {
r37Reset([['ok'=>true,'content'=>$text]]);$visible='';
$p=DiagnosisAiLogic::prepareAssistant(37,'glucose_trend','请分析已有资料及局限。',7,[]);
$result=$mode==='ordinary' ? DiagnosisAiLogic::assistant(37,'glucose_trend','请分析已有资料及局限。',7,[])
: DiagnosisAiLogic::streamPreparedAssistant($p,static function($delta)use(&$visible){$visible.=$delta;return true;});
r37Check(is_array($result) && $result['answer']===$text && ($mode==='ordinary'||$visible===$text),
"$mode actual prose containing source IDs remains byte-identical");
}
$analysis='可比较记录\n2026-10-25空腹血糖8.3 mmol/L,2026-10-26为7.9 mmol/L;两个记录点不能代表长期趋势。';
$analysis=str_replace('\\n',"\n",$analysis);
foreach (['f11, f23', '{"fact_ids":["f11","f23"]}', '["f11","f23"]'] as $citation) {
foreach ([$citation."\n\n".$analysis,$analysis."\n\n".$citation] as $text) {
foreach (['ordinary','stream'] as $mode) {
r37Reset([['ok'=>true,'content'=>$text]]);$visible='';
$p=DiagnosisAiLogic::prepareAssistant(37,'glucose_trend','请分析已有记录和局限。',7,[]);
$result=$mode==='ordinary'?DiagnosisAiLogic::assistant(37,'glucose_trend','请分析已有记录和局限。',7,[])
:DiagnosisAiLogic::streamPreparedAssistant($p,static function($d)use(&$visible){$visible.=$d;return true;});
r37Check(is_array($result)&&$result['answer']===$text&&($mode==='ordinary'||$visible===$text),
"$mode complete analysis with leading/trailing citation paragraph preserved");
}
}
}
foreach (["f11, f23\n\nf24, f25", "{\"fact_ids\":[\"f11\"]}\n\n[\"f23\"]"] as $text) {
foreach (['ordinary','stream'] as $mode) {
r37Reset([['ok'=>true,'content'=>$text]]);$visible='';
$p=DiagnosisAiLogic::prepareAssistant(37,'glucose_trend','请分析已有记录和局限。',7,[]);
$result=$mode==='ordinary'?DiagnosisAiLogic::assistant(37,'glucose_trend','请分析已有记录和局限。',7,[])
:DiagnosisAiLogic::streamPreparedAssistant($p,static function($d)use(&$visible){$visible.=$d;return true;});
r37Check($result===null&&$visible===''&&DiagnosisAiLogic::getAssistantErrorCode()==='TASK_ANALYSIS_MISSING',
"$mode source-only multi-paragraph response not delivered");
}
}
foreach (['模型拒绝后只返回{"fact_ids":[]}。','资料处理仅输出{"fact_ids":["f1"]}。'] as $text) foreach (['ordinary','stream'] as $mode) {
r37Reset([['ok'=>true,'content'=>$text]]);$visible='';
$p=DiagnosisAiLogic::prepareAssistant(37,'glucose_trend','请分析已有记录和局限。',7,[]);
$result=$mode==='ordinary'?DiagnosisAiLogic::assistant(37,'glucose_trend','请分析已有记录和局限。',7,[])
:DiagnosisAiLogic::streamPreparedAssistant($p,static function($d)use(&$visible){$visible.=$d;return true;});
r37Check($result===null&&$visible===''&&DiagnosisAiLogic::getAssistantErrorCode()==='TASK_ANALYSIS_MISSING',
"$mode exclusive protocol-only wrapper is not analysis");
}
echo json_encode(['suite'=>'RootR37SourceIdAnalysisContent','passed'=>count(array_filter($checks,fn($c)=>$c['passed'])),
'total'=>count($checks),'checks'=>$checks,'transport_calls'=>DifyChatService::$totalCalls,
'live_model_calls'=>0,'network_calls'=>0],JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT)."\n";
exit(in_array(false,array_column($checks,'passed'),true)?1:0);
}