Commit 430c9503 by luoqi

refactor(script): ScriptContext.patient 结构化 — 数据全集(非审计,无 ID)

ScriptContext = 三档全集数据(tier-agnostic superset),各档后续组装 user prompt 各取所需;
**不是审计快照**(关联走 agent_invocations.linked_patient_id 列)。
- nameMasked → salutation 正名(它是"通话称呼"非脱敏名);消费方 pii/call/header/realtime-coach 同步
- 加 name(真名)— 数据,补"患者名字都没有"的缺;但**不进 user prompt**(走 token 回填)
- ID 类(externalId/id)不进(非审计,无需标识符)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent 7ff7bc19
......@@ -78,7 +78,7 @@ const safetyRules: ReadonlyArray<SafetyRule<DraftPlanScriptOutput>> = [
*/
function fallback(input: DraftPlanScriptInput): DraftPlanScriptOutput {
const { patient, clinicName, plan, clinicalContext } = input;
const salutation = patient.nameMasked; // 统一通话称呼(年龄+性别+监护人 aware,orchestrator 算好)
const salutation = patient.salutation; // 统一通话称呼(年龄+性别+监护人 aware,orchestrator 算好)
// 漏诊项 = PAC 应治未治 reason(取 priorityScore 最高的一条)→ 转换层归一
const topReason = [...(plan.reasons ?? [])].sort((a, b) => b.priorityScore - a.priorityScore)[0];
const doctor = topReason?.triggerDoctor ?? clinicalContext.primaryDoctorName ?? '您的主治医生';
......
......@@ -11,13 +11,17 @@
// 也被实时教练复用(realtime-coach-context)。脊柱 = AiCallRunner,策略 = 各档 AiCall。
// 设计见 docs/algorithm/ai-script-generation.md。
export interface ScriptContext {
/** 患者信息(已脱敏:nameMasked, phone 不传) */
// 患者数据(ScriptContext = 三档全集数据,各档组装 user prompt 时各取所需;非审计快照)。
// ⚠️ ID 类不进(关联走 agent_invocations.linked_patient_id 列);真名 name 是数据,
// 但**不进 user prompt**(走 token 回填),只供内部/深度档更个性化时取用。
patient: {
/** 通话称呼(单一源:年龄+性别+监护人 aware)— 未成年=监护人称呼/家长,成人=先生/女士 */
nameMasked: string;
/** 患者真名(如"张震校")— 数据;不直出 user prompt,话术称呼走 salutation/token */
name: string | null;
/** 通话称呼(单一源:年龄+性别+监护人 aware)— 未成年=监护人称呼/家长,成人=先生/女士。原 nameMasked */
salutation: string;
gender: string | null;
age: number | null;
/** 病历号(host 病历主键,如 "FY0A000922")— 话术里供客服核对身份用,可空 */
/** 病历号(host 病历主键,如 "FY0A000922")— 供客服核对身份,可空 */
medicalRecordNumber?: string | null;
/** 监护人(未成年触达对象)— 让话术知道"打给家长、患者是孩子(称宝宝)";成人/无监护人 → null */
guardian?: { relationshipLabel: string; name: string | null } | null;
......
......@@ -26,7 +26,7 @@ export const NAME_TOKEN = {
export function realNames(input: ScriptContext): { callName: string; doctor: string; agent: string | null } {
const top = [...(input.plan.reasons ?? [])].sort((a, b) => b.priorityScore - a.priorityScore)[0];
return {
callName: input.patient.nameMasked, // 已是通话称呼(徐女士/张家长/张先生)
callName: input.patient.salutation, // 已是通话称呼(徐女士/张家长/张先生)
doctor: top?.triggerDoctor ?? input.clinicalContext.primaryDoctorName ?? '您的主治医生',
agent: input.agent?.name ?? null,
};
......
......@@ -135,7 +135,7 @@ export class PlanScriptOrchestrator {
// ─── 3. 回填人名 token(LLM 输出脱敏的反向)→ 渲染 Markdown ───
const finalOutput = detokenizeScript(result.output, input);
const content = renderMarkdown(finalOutput, { patientNameMasked: input.patient.nameMasked });
const content = renderMarkdown(finalOutput, { patientNameMasked: input.patient.salutation });
// ─── 4. 写 PlanScript(dryRun 时跳过) ───
let planScriptId: string | null = null;
......@@ -212,7 +212,7 @@ export class PlanScriptOrchestrator {
evalMode: 'production',
});
const patientNameMasked = input.patient.nameMasked;
const patientNameMasked = input.patient.salutation;
let lastStructured: DraftPlanScriptOutput | null = null;
let lastInvocationId: string | null = null;
let lastSource: 'agent' | 'template_fallback' = 'agent';
......@@ -411,9 +411,10 @@ export class PlanScriptOrchestrator {
return {
patient: {
// ⭐ 通话称呼(单一源,年龄+性别+监护人 aware):未成年→打给监护人(妈妈→"X女士"),
// 无监护人姓名→"X家长";成人→"X先生/女士"。header/开场白共用此值,不再各算一遍。
nameMasked: callSalutation(patient.name, patient.gender, patientAge, guardian ?? null),
// 真名(数据;不直出 user prompt,走 token 回填)。ID 类不进(关联走 linked_patient_id 列)
name: patient.name,
// 通话称呼(单一源,年龄+性别+监护人 aware):未成年→监护人("X女士")/家长,成人→先生女士
salutation: callSalutation(patient.name, patient.gender, patientAge, guardian ?? null),
gender: patient.gender,
age: patientAge,
medicalRecordNumber: patient.medicalRecordNumber ?? null,
......
......@@ -55,7 +55,7 @@ export class RealtimeCoachContextService {
const persona = input.personaHighlights.map((h) => `${h.label}:${h.description}`).join(' · ');
return [
'# 当前通话患者背景(真实数据,供你聊天时取用,不要编造、别照着推销)',
`- 诊所:${input.clinicName} · 称呼:${input.patient.nameMasked} · 年龄:${input.patient.age ?? '未知'}`,
`- 诊所:${input.clinicName} · 称呼:${input.patient.salutation} · 年龄:${input.patient.age ?? '未知'}`,
persona ? `- 画像:${persona}` : '',
`- 上次就诊发现 / 待处理:${cc.pendingTreatments.join('、') || input.plan.reasons[0]?.reason || '(无)'}`,
`- 主治医生:${cc.primaryDoctorName ?? '(未知)'} · 距上次到店:${cc.daysSinceLastVisit ?? '未知'} `,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment