79 lines
4.6 KiB
PHP
79 lines
4.6 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\adminapi\service;
|
||
|
||
use think\facade\Log;
|
||
|
||
/** Request-local lexical diagnostics; never a clinical validation decision. */
|
||
final class AssistantClinicalRejectDiagnostic
|
||
{
|
||
private const FORMS = ['pulse_row', 'missing_pulse_statement', 'workflow_rule_narration', 'pulse_feature', 'other'];
|
||
private const MAX_PROBE_CHARS = 100000;
|
||
private const FEATURES = '无力|有力|浮|沉|迟|数|虚|实|滑|涩|弦|细|洪|弱|缓|紧|濡|微|促|结|代|正常|无异常';
|
||
private const PULSE = '脉(?:象|诊)?(?!率|压|搏|络|管)|舌脉|\bpulse\b(?![\s_-]*(?:rate|pressure)\b)';
|
||
|
||
/** @return list<string> Fixed lexical forms only; unrecognized pulse wording stays other. */
|
||
public static function forms(string $unit): array
|
||
{
|
||
if (!mb_check_encoding($unit, 'UTF-8')) return ['other'];
|
||
$found = [];
|
||
$probe = mb_substr($unit, 0, self::MAX_PROBE_CHARS, 'UTF-8');
|
||
// Only a complete, narrow workflow-rule clause is masked here. A nearby
|
||
// request to collect pulse remains visible to the other form checks.
|
||
$probe = preg_replace_callback('/[^,,。;;!!??\r\n]+/u', static function (array $match) use (&$found): string {
|
||
$clause = trim($match[0], " \t|*");
|
||
$workflow = '(?:本流程|当前流程|本工作流|当前工作流|本次问诊|此次问诊|本次评估|当前评估|问诊流程|咨询流程|线上问诊|远程问诊)';
|
||
$noun = '(?:脉象|脉诊|切脉|把脉|诊脉|舌脉)';
|
||
if (preg_match('/^(?:' . $workflow . '(?:中|下)?[::\s]*(?:不采集|不使用|不依据|不纳入|不进行|禁止采集|无需采集|无需补采|无需提供)'
|
||
. $noun . '(?:信息|资料|评估|辨证)?|'
|
||
. $noun . '(?:不纳入|不用于|不作为)' . $workflow . '(?:的)?(?:评估|依据|辨证)?)$/uD', $clause) === 1) {
|
||
$found['workflow_rule_narration'] = true;
|
||
return ' ';
|
||
}
|
||
return $match[0];
|
||
}, $probe) ?? $probe;
|
||
|
||
$row = '/^[ \t]*(?:\|[ \t]*)?(?:(?:[-*+]|\d+[.)、])[ \t]+)?(?:\*\*)?(?:脉象|脉诊|舌脉|脉)(?:\*\*)?[ \t]*(?:[::]|\|)/mu';
|
||
if (preg_match($row, $probe) === 1) $found['pulse_row'] = true;
|
||
$missing = '/(?:(?:脉象|脉诊|脉(?!率|压|搏|络|管))[\s::|*]*(?:未记录|未采集|缺失|缺少|缺乏|未提供|未见记录|未获取|暂无|不详|待补|资料不足|信息不足)'
|
||
. '|(?:缺|缺少|缺乏|未采集|未记录|未提供|未获取)(?:的)?(?:脉象|脉诊|脉(?!率|压|搏|络|管))'
|
||
. '|\bpulse\b(?![\s_-]*(?:rate|pressure)\b)[\s:]*(?:(?:is|was)[\s]+)?(?:not[\s]+(?:recorded|collected|provided)|missing|unavailable))/iu';
|
||
$feature = '/(?:脉(?:象|诊)?[::\s|*]*(?:(?:呈|为|见|可见|表现为|按之)[::\s]*)?(?:不|未|无)?(?:' . self::FEATURES . ')'
|
||
. '|(?:' . self::FEATURES . ')脉(?!络|管|压|率))/u';
|
||
foreach (['missing_pulse_statement' => $missing, 'pulse_feature' => $feature] as $form => $pattern) {
|
||
if (preg_match($pattern, $probe) !== 1) continue;
|
||
$found[$form] = true;
|
||
$probe = preg_replace($pattern, ' ', $probe) ?? $probe;
|
||
}
|
||
// A row label is already represented, but any unclassified pulse wording
|
||
// (including active solicitation) gets other, even in a mixed paragraph.
|
||
$probe = preg_replace($row, ' ', $probe) ?? $probe;
|
||
if ($found === [] || preg_match('/(?:' . self::PULSE . ')/iu', $probe) === 1) $found['other'] = true;
|
||
return array_values(array_filter(self::FORMS, static fn (string $form): bool => isset($found[$form])));
|
||
}
|
||
|
||
/** @return array{trace_id:string,pulse_forms:list<string>,clinical_issue_codes:list<string>,unit_chars:int} */
|
||
public static function event(AssistantSseTiming $timing, string $unit, array $issues): array
|
||
{
|
||
$codes = [];
|
||
foreach ($issues as $issue) {
|
||
if (is_array($issue) && is_string($issue['code'] ?? null)) $codes[] = $issue['code'];
|
||
}
|
||
// Reuse the existing exact issue allowlist and its sixteen-code bound.
|
||
$safe = $timing->event('upstream_done', ['clinical_issue_codes' => $codes]);
|
||
return [
|
||
'trace_id' => $timing->id(),
|
||
'pulse_forms' => self::forms($unit),
|
||
'clinical_issue_codes' => $safe['clinical_issue_codes'],
|
||
'unit_chars' => mb_strlen($unit, 'UTF-8'),
|
||
];
|
||
}
|
||
|
||
public static function log(AssistantSseTiming $timing, string $unit, array $issues): void
|
||
{
|
||
Log::write('diagnosis ai clinical rejection ' . json_encode(self::event($timing, $unit, $issues)), 'info');
|
||
}
|
||
}
|