更新
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service\prescriptionai;
|
||||
|
||||
final class PrescriptionAiPolicy
|
||||
{
|
||||
public const MODELS = ['qwen', 'openai'];
|
||||
public const ACTIVE_TASKS = ['queued', 'running', 'retry_wait'];
|
||||
public const CLINICAL_FIELDS = [
|
||||
'diagnosis_id', 'patient_id', 'gender', 'age', 'prescription_type', 'herbs',
|
||||
'clinical_diagnosis', 'tongue', 'tongue_image', 'pulse', 'pulse_condition',
|
||||
'dose_count', 'dose_unit', 'dosage_amount', 'dosage_unit', 'dosage_bag_count',
|
||||
'need_decoction', 'bags_per_dose', 'usage_days', 'times_per_day', 'aux_usage',
|
||||
'usage_instruction', 'usage_time', 'usage_way', 'dietary_taboo', 'usage_notes',
|
||||
];
|
||||
|
||||
public static function decode($value): array
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
$decoded = is_string($value) ? json_decode($value, true) : null;
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
public static function canonical($value): string
|
||||
{
|
||||
$sort = static function ($item) use (&$sort) {
|
||||
if (!is_array($item)) {
|
||||
return $item;
|
||||
}
|
||||
if (!array_is_list($item)) {
|
||||
ksort($item, SORT_STRING);
|
||||
}
|
||||
return array_map($sort, $item);
|
||||
};
|
||||
return json_encode($sort($value), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
public static function clinical(array $row): array
|
||||
{
|
||||
$value = array_intersect_key($row, array_flip(self::CLINICAL_FIELDS));
|
||||
foreach (['herbs', 'aux_usage'] as $field) {
|
||||
if (array_key_exists($field, $value)) {
|
||||
$value[$field] = self::decode($value[$field]);
|
||||
}
|
||||
}
|
||||
foreach (['diagnosis_id', 'patient_id', 'gender', 'age', 'dose_count', 'dosage_bag_count',
|
||||
'need_decoction', 'bags_per_dose', 'usage_days', 'times_per_day'] as $field) {
|
||||
if (array_key_exists($field, $value)) {
|
||||
$value[$field] = (int) $value[$field];
|
||||
}
|
||||
}
|
||||
if (isset($value['dosage_amount']) && is_numeric($value['dosage_amount'])) {
|
||||
$value['dosage_amount'] = (float) $value['dosage_amount'];
|
||||
}
|
||||
if (isset($value['herbs'])) {
|
||||
foreach ($value['herbs'] as &$herb) {
|
||||
if (!is_array($herb)) {
|
||||
continue;
|
||||
}
|
||||
$herb = array_intersect_key($herb, array_flip([
|
||||
'name', 'medicine_id', 'id', 'dosage', 'unit', 'dose_basis',
|
||||
'processing', 'formula_type', 'special_usage', 'instructions', 'usage', 'remark',
|
||||
]));
|
||||
foreach (['dosage', 'medicine_id', 'id'] as $field) {
|
||||
if (isset($herb[$field]) && is_numeric($herb[$field])) {
|
||||
$herb[$field] = (float) $herb[$field];
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($herb);
|
||||
// Row order has no clinical meaning; do not deduplicate or drop invalid rows.
|
||||
usort($value['herbs'], static fn ($a, $b): int => strcmp(self::canonical($a), self::canonical($b)));
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
public static function fingerprint(array $row): string
|
||||
{
|
||||
return hash('sha256', self::canonical(self::clinical($row)));
|
||||
}
|
||||
|
||||
public static function isManual(array $row): bool
|
||||
{
|
||||
return (int) ($row['id'] ?? 0) > 0
|
||||
&& empty($row['delete_time']) && (int) ($row['void_status'] ?? 0) === 0
|
||||
&& (int) ($row['is_system_auto'] ?? 0) === 0
|
||||
&& self::decode($row['herbs'] ?? []) !== [];
|
||||
}
|
||||
|
||||
public static function aggregate(array $states): string
|
||||
{
|
||||
if ($states === []) {
|
||||
return 'preparing';
|
||||
}
|
||||
if (array_intersect($states, self::ACTIVE_TASKS) !== []) {
|
||||
return 'running';
|
||||
}
|
||||
$success = count(array_filter($states, static fn ($v): bool => $v === 'success'));
|
||||
if ($success === 2) {
|
||||
return 'success';
|
||||
}
|
||||
if ($success > 0) {
|
||||
return 'partial';
|
||||
}
|
||||
return count(array_filter($states, static fn ($v): bool => $v === 'cancelled')) === count($states)
|
||||
? 'cancelled' : 'failed';
|
||||
}
|
||||
|
||||
public static function retryAt(int $attempts, int $now, bool $retryable, int $maxAttempts): ?int
|
||||
{
|
||||
return $retryable && $attempts < $maxAttempts ? $now + ($attempts <= 1 ? 30 : 120) : null;
|
||||
}
|
||||
|
||||
public static function errorMessage(string $code): string
|
||||
{
|
||||
return match ($code) {
|
||||
'PATIENT_BINDING_REQUIRED' => '需完善患者与诊单关联',
|
||||
'ACCESS_REVOKED' => '资料访问权限已变更',
|
||||
'SOURCE_CHANGED' => '资料或处方已更新,请查看新版本',
|
||||
'BUDGET_PAUSED' => '已达到分析预算,等待额度恢复',
|
||||
'CONFIG_INVALID', 'CONFIG_DISABLED', 'UPSTREAM_AUTH_FAILED' => '模型配置不可用,请联系管理员',
|
||||
'UPSTREAM_TIMEOUT' => '模型响应超时,可稍后重试',
|
||||
'INVALID_RESPONSE', 'RESPONSE_INVALID', 'INVALID_EVIDENCE_OUTPUT', 'INVALID_FILE_EVIDENCE_OUTPUT', 'INVALID_REPORT_OUTPUT' => '模型返回内容未通过校验',
|
||||
'CONTEXT_TOO_LARGE', 'INPUT_TOKEN_BUDGET_EXCEEDED', 'SYNTHESIS_BUDGET_EXCEEDED' => '资料超过本次处理预算',
|
||||
'FINAL_CONTEXT_EXCEEDS_BUDGET' => '资料来源与缺口说明超过汇总预算,请联系管理员调整处理配置',
|
||||
'LEASE_EXPIRED' => '工作进程中断,任务等待恢复',
|
||||
default => '分析暂未完成,可查看资料缺口或重试',
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user