更新
This commit is contained in:
@@ -71,7 +71,7 @@ assistantStreamExpect(
|
||||
$actionStart = strpos($controller, 'public function aiAssistantStream()');
|
||||
$checkAt = strpos($controller, "goCheck('aiAssistant')", $actionStart);
|
||||
$prepareAt = strpos($controller, 'DiagnosisAiLogic::prepareAssistant(', $actionStart);
|
||||
$runAt = strpos($controller, '$this->runAssistantSse($prepared)', $actionStart);
|
||||
$runAt = strpos($controller, '$this->runAssistantSse($prepared, $timing)', $actionStart);
|
||||
$headerAt = strpos($controller, "header('Content-Type: text/event-stream; charset=utf-8')", $actionStart);
|
||||
assistantStreamExpect(
|
||||
$actionStart !== false && $checkAt > $actionStart && $prepareAt > $checkAt && $runAt > $prepareAt && $headerAt > $runAt,
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
use app\adminapi\service\AssistantSseTiming;
|
||||
|
||||
function timingExpect(bool $condition, string $message): void
|
||||
{
|
||||
if (!$condition) {
|
||||
fwrite(STDERR, "FAIL: {$message}\n");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
$one = new AssistantSseTiming();
|
||||
$two = new AssistantSseTiming();
|
||||
$first = $one->event('prepare', [
|
||||
'duration_ms' => 120,
|
||||
'aggregate_ms' => 75,
|
||||
'compaction_ms' => 0,
|
||||
'source_bytes' => 1000,
|
||||
'query_bytes' => 1100,
|
||||
'attachment_count' => 2,
|
||||
'compaction_attempted' => false,
|
||||
'status' => 'ok',
|
||||
'patient_name' => '患者姓名',
|
||||
'query' => '患者正文',
|
||||
'url' => 'https://private.example/path',
|
||||
'api_key' => 'secret',
|
||||
]);
|
||||
$second = $one->event('upstream_done', [
|
||||
'duration_ms' => 4300,
|
||||
'retry_ms' => 1200,
|
||||
'retry_attempted' => true,
|
||||
'delta_count' => 4,
|
||||
'delta_bytes' => 24,
|
||||
'status' => 'error',
|
||||
'error_code' => 'UPSTREAM_TIMEOUT',
|
||||
]);
|
||||
|
||||
timingExpect(preg_match('/^[a-f0-9]{32}$/', $first['trace_id']) === 1, 'trace id is random hex only');
|
||||
timingExpect($first['trace_id'] !== $two->event('prepare')['trace_id'], 'requests use distinct trace ids');
|
||||
timingExpect($first['trace_id'] === $second['trace_id'], 'all phases share one trace id');
|
||||
timingExpect(
|
||||
$first['aggregate_ms'] === 75 && $first['attachment_count'] === 2 && $first['compaction_attempted'] === false,
|
||||
'prepare phase retains useful numeric metadata'
|
||||
);
|
||||
timingExpect(
|
||||
$second['retry_attempted'] === true && $second['retry_ms'] === 1200
|
||||
&& $second['error_code'] === 'UPSTREAM_TIMEOUT',
|
||||
'upstream phase retains retry, duration and safe status'
|
||||
);
|
||||
$encoded = json_encode($first, JSON_UNESCAPED_UNICODE);
|
||||
foreach (['患者姓名', '患者正文', 'private.example', 'secret'] as $sensitive) {
|
||||
timingExpect(!str_contains($encoded, $sensitive), 'timing log excludes sensitive payload');
|
||||
}
|
||||
timingExpect(!isset($one->event('prepare', ['duration_ms' => '患者正文'])['duration_ms']), 'numeric fields reject text');
|
||||
timingExpect(!isset($one->event('upstream_done', ['error_code' => 'KEY=secret'])['error_code']), 'error code is validated');
|
||||
timingExpect(!isset($one->event('upstream_done', ['error_code' => 'API_KEY_SECRET'])['error_code']), 'unknown machine codes are not logged');
|
||||
|
||||
$startedAt = hrtime(true);
|
||||
timingExpect(AssistantSseTiming::elapsedMs($startedAt) >= 0, 'monotonic elapsed time is non-negative');
|
||||
|
||||
$controller = file_get_contents(dirname(__DIR__) . '/app/adminapi/controller/tcm/DiagnosisController.php');
|
||||
$logic = file_get_contents(dirname(__DIR__) . '/app/adminapi/logic/tcm/DiagnosisAiLogic.php');
|
||||
timingExpect(is_string($controller) && is_string($logic), 'instrumented source is readable');
|
||||
timingExpect(str_contains($controller, "log('first_delta'") && str_contains($controller, "log('sse_done'"), 'SSE first delta and completion are timed');
|
||||
timingExpect(str_contains($logic, "log('prepare'") && str_contains($logic, "log('upstream_done'"), 'prepare and upstream are timed');
|
||||
timingExpect(str_contains($logic, 'compaction_attempted') && str_contains($logic, 'retry_attempted'), 'compression and compatibility retry are recorded');
|
||||
|
||||
echo "Diagnosis AI assistant timing: OK\n";
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service {
|
||||
function config(string $name): array
|
||||
{
|
||||
return $GLOBALS['ancientReuseConfig'];
|
||||
}
|
||||
|
||||
function curl_init(): \stdClass
|
||||
{
|
||||
return new \stdClass();
|
||||
}
|
||||
|
||||
function curl_setopt_array(\stdClass $handle, array $options): bool
|
||||
{
|
||||
$handle->options = $options;
|
||||
$GLOBALS['ancientReuseRequests'][] = json_decode($options[CURLOPT_POSTFIELDS], true);
|
||||
return true;
|
||||
}
|
||||
|
||||
function curl_exec(\stdClass $handle): string
|
||||
{
|
||||
$options = $handle->options;
|
||||
if (isset($options[CURLOPT_WRITEFUNCTION])) {
|
||||
$options[CURLOPT_HEADERFUNCTION]($handle, "HTTP/1.1 200 OK\r\n");
|
||||
$options[CURLOPT_WRITEFUNCTION]($handle,
|
||||
"data: {\"event\":\"message\",\"answer\":\"offline\"}\n\n"
|
||||
. "data: {\"event\":\"message_end\"}\n\n");
|
||||
return '';
|
||||
}
|
||||
return '{"answer":"offline"}';
|
||||
}
|
||||
|
||||
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\TcmAncientBooksReference;
|
||||
|
||||
function ancientReuseExpect(bool $condition, string $message): void
|
||||
{
|
||||
if (!$condition) {
|
||||
fwrite(STDERR, "FAIL: {$message}\n");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
$indexProperty = new ReflectionProperty(TcmAncientBooksReference::class, 'index');
|
||||
$indexProperty->setValue(null, [
|
||||
'commit' => TcmAncientBooksReference::COMMIT,
|
||||
'index_version' => TcmAncientBooksReference::INDEX_VERSION,
|
||||
'terms' => [
|
||||
'消渴' => [[
|
||||
'title' => '甲书', 'chapter' => '消渴篇', 'path' => '甲书.txt',
|
||||
'line' => 42, 'excerpt' => '消渴原文。', 'score' => 15,
|
||||
]],
|
||||
],
|
||||
'chapter_terms' => [
|
||||
'痛风' => [[
|
||||
'title' => '乙书', 'chapter' => '痛风篇', 'path' => '乙书.txt',
|
||||
'line' => 88, 'excerpt' => '痛风原文。', 'score' => 12,
|
||||
]],
|
||||
],
|
||||
]);
|
||||
$ancientReuseConfig = [
|
||||
'enable' => true, 'base_url' => 'https://ai.example.test/v1/chat-messages',
|
||||
'timeout' => 30, 'models' => ['qwen' => ['name' => 'offline', 'api_key' => 'test-key']],
|
||||
];
|
||||
|
||||
foreach (['患者糖尿病,请分析。', '患者痛风,请分析。', '患者有未收录的症状。'] as $case) {
|
||||
$expectedSources = ClinicalKnowledgeReference::ancientBookSources($case);
|
||||
$expectedPrompt = ClinicalKnowledgeReference::augmentQuery($case);
|
||||
foreach (['blocking', 'streaming'] as $mode) {
|
||||
$ancientReuseRequests = [];
|
||||
if ($mode === 'blocking') {
|
||||
$result = DifyChatService::chat('qwen', [], $case, 'offline-user');
|
||||
} else {
|
||||
$deltas = [];
|
||||
$result = DifyChatService::streamChat('qwen', [], $case, 'offline-user',
|
||||
static function (string $delta) use (&$deltas): void { $deltas[] = $delta; });
|
||||
ancientReuseExpect($deltas === ['offline'], 'streaming forwards the upstream delta');
|
||||
}
|
||||
ancientReuseExpect($result['ok'] === true, "$mode request succeeds");
|
||||
ancientReuseExpect(count($ancientReuseRequests) === 1, "$mode sends one request");
|
||||
ancientReuseExpect($ancientReuseRequests[0]['query'] === $expectedPrompt,
|
||||
"$mode prompt remains byte-for-byte identical");
|
||||
ancientReuseExpect($result['ancient_book_sources'] === $expectedSources,
|
||||
"$mode returns exactly the selected citations");
|
||||
}
|
||||
}
|
||||
|
||||
$indexProperty->setValue(null, null);
|
||||
echo "Dify ancient-books reuse contract passed.\n";
|
||||
}
|
||||
@@ -52,6 +52,20 @@ ancientExpect(str_contains($augmented, NihaixiaClinicalSkill::VERSION), 'existin
|
||||
ancientExpect(str_ends_with($augmented, $query), 'original patient question remains intact');
|
||||
ancientExpect(ClinicalKnowledgeReference::augmentQuery($augmented) === $augmented, 'retry does not duplicate references');
|
||||
|
||||
foreach ([$query, '患者出现痛风症状。', '患者有未收录的症状。', '阶段=text 患者糖尿病', '阶段=final 患者糖尿病'] as $case) {
|
||||
$selected = ClinicalKnowledgeReference::ancientBookSources($case);
|
||||
ancientExpect(
|
||||
ClinicalKnowledgeReference::augmentQueryWithAncientBookSources($case, $selected)
|
||||
=== ClinicalKnowledgeReference::augmentQuery($case),
|
||||
'selected sources preserve the exact legacy prompt for each clinical case'
|
||||
);
|
||||
}
|
||||
ancientExpect(
|
||||
ClinicalKnowledgeReference::augmentQueryWithAncientBookSources('患者有未收录的症状。', [])
|
||||
=== ClinicalKnowledgeReference::augmentQuery('患者有未收录的症状。'),
|
||||
'an unrelated request receives no preceding patient citations'
|
||||
);
|
||||
|
||||
$messages = [['role' => 'user', 'content' => $query]];
|
||||
$wire = ClinicalKnowledgeReference::augmentMessages($messages);
|
||||
ancientExpect(count($wire) === 2 && str_contains($wire[0]['content'], '测试古籍'), 'chat receives reference in system message');
|
||||
|
||||
Reference in New Issue
Block a user