94 lines
5.8 KiB
PHP
94 lines
5.8 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
// Exercise the real transport's query/profile/stream construction without a network.
|
||
namespace app\common\service {
|
||
function config(string $name): array { return $GLOBALS['r37TransportConfig']; }
|
||
function curl_init(): \stdClass { return new \stdClass(); }
|
||
function curl_setopt_array(\stdClass $handle, array $options): bool
|
||
{
|
||
$handle->options = $options;
|
||
$GLOBALS['r37TransportRequests'][] = json_decode($options[CURLOPT_POSTFIELDS], true, 512, JSON_THROW_ON_ERROR);
|
||
$GLOBALS['r37TransportHeaders'][] = $options[CURLOPT_HTTPHEADER];
|
||
return true;
|
||
}
|
||
function curl_exec(\stdClass $handle): string
|
||
{
|
||
$answer = $GLOBALS['r37TransportAnswer'];
|
||
if (isset($handle->options[CURLOPT_WRITEFUNCTION])) {
|
||
$handle->options[CURLOPT_HEADERFUNCTION]($handle, "HTTP/1.1 200 OK\r\n");
|
||
$handle->options[CURLOPT_WRITEFUNCTION]($handle,
|
||
'data: ' . json_encode(['event' => 'message', 'answer' => $answer], JSON_UNESCAPED_UNICODE) . "\n\n"
|
||
. "data: {\"event\":\"message_end\"}\n\n");
|
||
return '';
|
||
}
|
||
return json_encode(['answer' => $answer], JSON_UNESCAPED_UNICODE);
|
||
}
|
||
function curl_errno(\stdClass $handle): int { return 0; }
|
||
function curl_getinfo(\stdClass $handle, int $option): int { return 200; }
|
||
function curl_close(\stdClass $handle): void {}
|
||
}
|
||
|
||
namespace {
|
||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||
|
||
use app\common\service\ClinicalKnowledgeReference;
|
||
use app\common\service\DifyChatService;
|
||
use app\common\service\TcmClinicalResponsePolicy;
|
||
|
||
$checks = 0;
|
||
function r37TransportExpect(bool $ok, string $message): void
|
||
{
|
||
++$GLOBALS['checks'];
|
||
if (!$ok) throw new \RuntimeException($message);
|
||
}
|
||
$r37TransportConfig = ['enable' => true, 'base_url' => 'https://no-network.invalid/v1/chat-messages',
|
||
'timeout' => 30, 'models' => [
|
||
'qwen' => ['name' => 'synthetic-qwen-original', 'api_key' => 'synthetic-qwen'],
|
||
'openai' => ['name' => 'synthetic-openai-original', 'api_key' => 'synthetic-openai'],
|
||
]];
|
||
$tasks = ['case_analysis' => '分析病历', 'glucose_trend' => '血糖趋势',
|
||
'medication_review' => '用药评估', 'complication_risk' => '并发症风险',
|
||
'summary' => '生成问诊小结', 'lifestyle_advice' => '健康建议'];
|
||
$raw = '2026-10-10空腹血糖8.2mmol/L;2026-10-12空腹血糖7.6mmol/L。舌象未记录。当前用药未记录。SOURCE_TAIL';
|
||
$policy = TcmClinicalResponsePolicy::consultationReference();
|
||
r37TransportExpect(str_contains($policy, '完成本次选定任务') && str_contains($policy, '缺项只限定依赖该项的判断'),
|
||
'missing data qualify dependent conclusions without disabling other analysis');
|
||
r37TransportExpect(str_contains($policy, '不将整个回答改成资料核对') && str_contains($policy, '未记录、未采集与不详保持未知'),
|
||
'task answers are analysis and retain unknown states');
|
||
r37TransportExpect(str_contains($policy, '脉象不参与本次辨证') && str_contains($policy, '不擅停换'),
|
||
'pulse exclusion and medication boundaries remain');
|
||
r37TransportExpect(str_contains(TcmClinicalResponsePolicy::reference(), '舌象、脉象各单列'),
|
||
'nonconsultation reference behavior stays separate');
|
||
|
||
foreach ($tasks as $task => $label) foreach (['qwen', 'openai'] as $profile) foreach (['blocking', 'streaming'] as $mode) {
|
||
$r37TransportRequests = []; $r37TransportHeaders = []; $deltas = [];
|
||
$r37TransportAnswer = "MODEL::{$task}::{$profile}::{$mode} 已分析现有资料;舌象未记录,相关判断保留不确定性。";
|
||
$query = "任务:{$label}\n请根据已有资料分析本任务,缺项明示。\n<CASE_DATA>{$raw}</CASE_DATA>";
|
||
$expectedQuery = ClinicalKnowledgeReference::augmentCaseQuery($query, true);
|
||
$inputs = ['task_marker' => $task, 'case_raw' => $raw];
|
||
$options = ['knowledge_mode' => 'clinical', 'reference_scope' => 'consultation'];
|
||
if ($mode === 'blocking') {
|
||
$result = DifyChatService::chat($profile, $inputs, $query, 'synthetic-r37-user', [], $options);
|
||
} else {
|
||
$result = DifyChatService::streamChat($profile, $inputs, $query, 'synthetic-r37-user',
|
||
static function (string $delta) use (&$deltas): void { $deltas[] = $delta; }, null, [], $options);
|
||
r37TransportExpect(implode('', $deltas) === $r37TransportAnswer, "{$task}/{$profile} streaming retains actual model text");
|
||
}
|
||
r37TransportExpect($result['ok'] === true && count($r37TransportRequests) === 1,
|
||
"{$task}/{$profile}/{$mode} invokes transport exactly once");
|
||
r37TransportExpect($result['content'] === $r37TransportAnswer,
|
||
"{$task}/{$profile}/{$mode} retains model answer without a source-only replacement");
|
||
r37TransportExpect($r37TransportRequests[0]['query'] === $expectedQuery && str_ends_with($expectedQuery, $query)
|
||
&& $r37TransportRequests[0]['inputs'] === $inputs,
|
||
"{$task}/{$profile}/{$mode} preserves task question and all raw inputs");
|
||
r37TransportExpect(in_array('Authorization: Bearer synthetic-' . $profile, $r37TransportHeaders[0], true),
|
||
"{$task}/{$profile}/{$mode} preserves configured provider selection");
|
||
r37TransportExpect(ClinicalKnowledgeReference::augmentCaseQuery($expectedQuery, true) === $expectedQuery,
|
||
'case query augmentation remains idempotent');
|
||
}
|
||
echo json_encode(['passed' => $checks, 'transport_calls' => 24, 'network_calls' => 0,
|
||
'scope' => 'real Dify transport construction with in-process cURL fakes; no real model, clinical acceptance or deployment'], JSON_UNESCAPED_UNICODE) . "\n";
|
||
}
|