| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752 |
- from __future__ import annotations
- from pathlib import Path
- from docx import Document
- from docx.enum.section import WD_SECTION
- from docx.enum.table import WD_ALIGN_VERTICAL, WD_TABLE_ALIGNMENT
- from docx.enum.text import WD_ALIGN_PARAGRAPH
- from docx.oxml import OxmlElement
- from docx.oxml.ns import qn
- from docx.shared import Inches, Pt, RGBColor
- OUT = Path(__file__).with_name("医梦P0三智能体语音交互、补槽字段与调用契约_V1.0.docx")
- PURPLE = "2B1F99"
- PURPLE_DARK = "211477"
- TEAL = "138E91"
- BLUE = "2E74B5"
- INK = "1A1835"
- MUTED = "706D89"
- LIGHT = "F4F5FB"
- LIGHT_BLUE = "E8EEF5"
- SUCCESS = "168469"
- WARNING = "C46B13"
- RED = "C63B3B"
- WHITE = "FFFFFF"
- BORDER = "D9DEEA"
- FONT_CN = "STSong"
- FONT_LATIN = "Calibri"
- def set_run_font(run, size=11, bold=False, color=INK, italic=False, font=FONT_CN):
- run.font.name = font
- run._element.get_or_add_rPr().rFonts.set(qn("w:eastAsia"), font)
- run._element.get_or_add_rPr().rFonts.set(qn("w:ascii"), font)
- run._element.get_or_add_rPr().rFonts.set(qn("w:hAnsi"), font)
- run.font.size = Pt(size)
- run.font.bold = bold
- run.font.italic = italic
- run.font.color.rgb = RGBColor.from_string(color)
- def shade_cell(cell, color):
- tc_pr = cell._tc.get_or_add_tcPr()
- shd = tc_pr.find(qn("w:shd"))
- if shd is None:
- shd = OxmlElement("w:shd")
- tc_pr.append(shd)
- shd.set(qn("w:fill"), color)
- def set_cell_margins(cell, top=80, start=120, bottom=80, end=120):
- tc = cell._tc
- tc_pr = tc.get_or_add_tcPr()
- tc_mar = tc_pr.first_child_found_in("w:tcMar")
- if tc_mar is None:
- tc_mar = OxmlElement("w:tcMar")
- tc_pr.append(tc_mar)
- for m, value in (("top", top), ("start", start), ("bottom", bottom), ("end", end)):
- node = tc_mar.find(qn(f"w:{m}"))
- if node is None:
- node = OxmlElement(f"w:{m}")
- tc_mar.append(node)
- node.set(qn("w:w"), str(value))
- node.set(qn("w:type"), "dxa")
- def set_table_geometry(table, widths):
- total = sum(widths)
- table.autofit = False
- table.alignment = WD_TABLE_ALIGNMENT.LEFT
- tbl_pr = table._tbl.tblPr
- layout = tbl_pr.find(qn("w:tblLayout"))
- if layout is None:
- layout = OxmlElement("w:tblLayout")
- tbl_pr.append(layout)
- layout.set(qn("w:type"), "fixed")
- tbl_w = tbl_pr.find(qn("w:tblW"))
- if tbl_w is None:
- tbl_w = OxmlElement("w:tblW")
- tbl_pr.append(tbl_w)
- tbl_w.set(qn("w:w"), str(total))
- tbl_w.set(qn("w:type"), "dxa")
- tbl_ind = tbl_pr.find(qn("w:tblInd"))
- if tbl_ind is None:
- tbl_ind = OxmlElement("w:tblInd")
- tbl_pr.append(tbl_ind)
- tbl_ind.set(qn("w:w"), "120")
- tbl_ind.set(qn("w:type"), "dxa")
- grid = table._tbl.tblGrid
- for child in list(grid):
- grid.remove(child)
- for width in widths:
- col = OxmlElement("w:gridCol")
- col.set(qn("w:w"), str(width))
- grid.append(col)
- for row in table.rows:
- for idx, cell in enumerate(row.cells):
- width = widths[min(idx, len(widths) - 1)]
- tc_pr = cell._tc.get_or_add_tcPr()
- tc_w = tc_pr.find(qn("w:tcW"))
- if tc_w is None:
- tc_w = OxmlElement("w:tcW")
- tc_pr.append(tc_w)
- tc_w.set(qn("w:w"), str(width))
- tc_w.set(qn("w:type"), "dxa")
- cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
- set_cell_margins(cell)
- def add_table(doc, headers, rows, widths, header_fill=PURPLE):
- table = doc.add_table(rows=1, cols=len(headers))
- table.style = "Table Grid"
- table.rows[0]._tr.get_or_add_trPr().append(OxmlElement("w:tblHeader"))
- for idx, header in enumerate(headers):
- cell = table.rows[0].cells[idx]
- shade_cell(cell, header_fill)
- p = cell.paragraphs[0]
- p.alignment = WD_ALIGN_PARAGRAPH.CENTER
- p.paragraph_format.space_after = Pt(0)
- set_run_font(p.add_run(header), size=9.5, bold=True, color=WHITE)
- for row_data in rows:
- cells = table.add_row().cells
- for idx, value in enumerate(row_data):
- p = cells[idx].paragraphs[0]
- p.paragraph_format.space_after = Pt(0)
- p.paragraph_format.line_spacing = 1.12
- set_run_font(p.add_run(str(value)), size=9.2, color=INK)
- set_table_geometry(table, widths)
- doc.add_paragraph().paragraph_format.space_after = Pt(0)
- return table
- def add_para(doc, text="", size=11, bold=False, color=INK, align=None, after=6, italic=False):
- p = doc.add_paragraph()
- p.paragraph_format.space_before = Pt(0)
- p.paragraph_format.space_after = Pt(after)
- p.paragraph_format.line_spacing = 1.25
- if align is not None:
- p.alignment = align
- set_run_font(p.add_run(text), size=size, bold=bold, color=color, italic=italic)
- return p
- def add_bullets(doc, items):
- for item in items:
- p = doc.add_paragraph(style="List Bullet")
- p.paragraph_format.left_indent = Inches(0.375)
- p.paragraph_format.first_line_indent = Inches(-0.188)
- p.paragraph_format.space_after = Pt(4)
- p.paragraph_format.line_spacing = 1.25
- set_run_font(p.add_run(item), size=10.5)
- def add_numbered(doc, items):
- for item in items:
- p = doc.add_paragraph(style="List Number")
- p.paragraph_format.left_indent = Inches(0.375)
- p.paragraph_format.first_line_indent = Inches(-0.188)
- p.paragraph_format.space_after = Pt(4)
- p.paragraph_format.line_spacing = 1.25
- set_run_font(p.add_run(item), size=10.5)
- def add_callout(doc, label, text, color=PURPLE, fill=LIGHT):
- table = doc.add_table(rows=1, cols=2)
- table.style = "Table Grid"
- shade_cell(table.cell(0, 0), color)
- shade_cell(table.cell(0, 1), fill)
- p0 = table.cell(0, 0).paragraphs[0]
- p0.alignment = WD_ALIGN_PARAGRAPH.CENTER
- p0.paragraph_format.space_after = Pt(0)
- set_run_font(p0.add_run(label), size=10, bold=True, color=WHITE)
- p1 = table.cell(0, 1).paragraphs[0]
- p1.paragraph_format.space_after = Pt(0)
- p1.paragraph_format.line_spacing = 1.2
- set_run_font(p1.add_run(text), size=10, color=INK)
- set_table_geometry(table, [1260, 8100])
- doc.add_paragraph().paragraph_format.space_after = Pt(0)
- def add_code(doc, text):
- table = doc.add_table(rows=1, cols=1)
- table.style = "Table Grid"
- shade_cell(table.cell(0, 0), "F2F4F7")
- p = table.cell(0, 0).paragraphs[0]
- p.paragraph_format.space_after = Pt(0)
- p.paragraph_format.line_spacing = 1.0
- run = p.add_run(text)
- set_run_font(run, size=8.2, color=INK, font="Consolas")
- set_table_geometry(table, [9360])
- doc.add_paragraph().paragraph_format.space_after = Pt(0)
- def configure_styles(doc):
- styles = doc.styles
- normal = styles["Normal"]
- normal.font.name = FONT_CN
- normal._element.rPr.rFonts.set(qn("w:eastAsia"), FONT_CN)
- normal._element.rPr.rFonts.set(qn("w:ascii"), FONT_CN)
- normal._element.rPr.rFonts.set(qn("w:hAnsi"), FONT_CN)
- normal.font.size = Pt(11)
- normal.font.color.rgb = RGBColor.from_string(INK)
- normal.paragraph_format.space_after = Pt(6)
- normal.paragraph_format.line_spacing = 1.25
- for name, size, color, before, after in (
- ("Heading 1", 16, PURPLE, 18, 10),
- ("Heading 2", 13, PURPLE, 14, 7),
- ("Heading 3", 12, TEAL, 10, 5),
- ):
- st = styles[name]
- st.font.name = FONT_CN
- st._element.rPr.rFonts.set(qn("w:eastAsia"), FONT_CN)
- st._element.rPr.rFonts.set(qn("w:ascii"), FONT_CN)
- st._element.rPr.rFonts.set(qn("w:hAnsi"), FONT_CN)
- st.font.size = Pt(size)
- st.font.bold = True
- st.font.color.rgb = RGBColor.from_string(color)
- st.paragraph_format.space_before = Pt(before)
- st.paragraph_format.space_after = Pt(after)
- st.paragraph_format.keep_with_next = True
- for name in ("List Bullet", "List Number"):
- st = styles[name]
- st.font.name = FONT_CN
- st._element.rPr.rFonts.set(qn("w:eastAsia"), FONT_CN)
- st._element.rPr.rFonts.set(qn("w:ascii"), FONT_CN)
- st._element.rPr.rFonts.set(qn("w:hAnsi"), FONT_CN)
- st.font.size = Pt(10.5)
- def add_heading(doc, text, level=1):
- p = doc.add_heading(text, level=level)
- for run in p.runs:
- set_run_font(run, size={1: 16, 2: 13, 3: 12}[level], bold=True, color=PURPLE if level < 3 else TEAL)
- return p
- def add_page_number(paragraph):
- paragraph.alignment = WD_ALIGN_PARAGRAPH.RIGHT
- run = paragraph.add_run("第 ")
- set_run_font(run, size=9, color=MUTED)
- fld = OxmlElement("w:fldSimple")
- fld.set(qn("w:instr"), "PAGE")
- paragraph._p.append(fld)
- run2 = paragraph.add_run(" 页")
- set_run_font(run2, size=9, color=MUTED)
- doc = Document()
- configure_styles(doc)
- section = doc.sections[0]
- section.top_margin = Inches(0.82)
- section.bottom_margin = Inches(0.82)
- section.left_margin = Inches(1.0)
- section.right_margin = Inches(1.0)
- section.header_distance = Inches(0.42)
- section.footer_distance = Inches(0.42)
- header = section.header
- hp = header.paragraphs[0]
- hp.alignment = WD_ALIGN_PARAGRAPH.RIGHT
- set_run_font(hp.add_run("医梦 AI · P0 执行级附件"), size=8.5, color=MUTED)
- add_page_number(section.footer.paragraphs[0])
- # Cover
- add_para(doc, "医梦 AI", size=11, bold=True, color=TEAL, after=20)
- add_para(doc, "P0 三智能体语音交互、\n补槽字段与调用契约", size=26, bold=True, color=PURPLE_DARK, after=8)
- add_para(doc, "医梦患者智能服务门户(Adjutant)执行级附件", size=14, color=MUTED, after=22)
- add_callout(
- doc,
- "核心原则",
- "患者主要通过语音表达诉求;智能体负责识别意图、提取已知信息和持续补槽;必要信息未完整、安全检查未通过或用户未确认时,不得进入业务执行。",
- color=TEAL,
- fill="E9F7F7",
- )
- add_table(
- doc,
- ["项目", "内容"],
- [
- ("文件版本", "V1.0"),
- ("编制日期", "2026 年 7 月 26 日"),
- ("适用范围", "单医院 P0:智能分诊挂号、检验检查报告解读、中医舌诊"),
- ("约束来源", "《医梦智能体分级开发与平台演进规范 V1.0》与《医梦 P0 智能体 Demo 开发与 FastGPT 编排实施方案 V1.0》"),
- ("使用对象", "产品、客户端、FastGPT、Mock MCP、测试与演示负责人"),
- ],
- [1900, 7460],
- )
- add_para(doc, "本附件不替代两份上位规范,只负责把语音交互、补槽规则、系统调用和验收要求转换为可执行契约。", size=10, color=MUTED, italic=True, after=0)
- doc.add_page_break()
- # 1
- add_heading(doc, "1. 文档目的与执行边界", 1)
- add_para(doc, "本附件用于统一客户端、FastGPT Workflow、Mock MCP 和测试对“患者如何开始、智能体如何追问、什么时候允许进入下一步”的理解。P0 数据可以是 Mock,但状态、字段和错误语义必须稳定。")
- add_heading(doc, "1.1 本附件解决的问题", 2)
- add_bullets(doc, [
- "将语音明确为患者侧主要输入方式,文字、快捷选项、上传和卡片作为辅助交互。",
- "为三个智能体定义必填、条件必填、可选槽位,以及缺失、推断、确认、冲突和失效规则。",
- "定义客户端调用智能体、智能体调用 Mock MCP、智能体返回标准 UI 组件的统一协议。",
- "建立信息完整性门禁、医疗安全门禁和事务二次确认门禁。",
- "给出可重复演示脚本与金标验收用例。",
- ])
- add_heading(doc, "1.2 P0 不包含", 2)
- add_bullets(doc, [
- "真实医院 HIS、LIS、EMR、支付和生产号源。",
- "完整语音识别引擎训练;P0 可使用固定转写结果或浏览器语音能力。",
- "真实在线诊断、处方、正式急诊分级和自动临床决策。",
- "家庭成员代办界面;但请求模型必须区分 operatorId 与 subjectPatientId。",
- ])
- # 2
- add_heading(doc, "2. 统一语音交互契约", 1)
- add_heading(doc, "2.1 主交互链路", 2)
- add_callout(doc, "FLOW", "语音表达 → 转写回显 → 意图识别 → 槽位提取 → 安全检查 → 缺失字段追问 → 完整性摘要 → 用户确认 → Tool/MCP 调用 → 结果卡片 → 健康记录")
- add_heading(doc, "2.2 语音状态", 2)
- add_table(
- doc,
- ["状态", "患者侧表现", "系统动作"],
- [
- ("VOICE_READY", "显示“按住说话”", "等待患者输入"),
- ("LISTENING", "显示波形与取消入口", "采集语音;P0 可模拟"),
- ("TRANSCRIBING", "显示“正在识别”", "生成文字转写"),
- ("TRANSCRIPT_CONFIRM", "回显识别内容", "低置信度或关键字段需确认"),
- ("SLOT_FILLING", "语音追问 + 快捷回答", "一次只追问一个高优先级缺失项"),
- ("READY_TO_PROCEED", "显示结构化摘要", "所有门禁通过,等待确认"),
- ("PROCESSING", "显示进度和不可重复提交", "调用 Tool/MCP"),
- ("COMPLETED / FAILED", "结果或恢复操作", "保存任务结果与 traceId"),
- ],
- [2050, 3050, 4260],
- )
- add_heading(doc, "2.3 识别、确认与纠错规则", 2)
- add_bullets(doc, [
- "语音转写必须回显,患者可修改后再继续。",
- "患者说“不是明天,是后天”时,新值覆盖旧值,并使依赖旧条件的候选结果失效。",
- "日期、时间、科室、医生、指标名称必须保留 rawValue 与 normalizedValue。",
- "低置信度关键字段不得静默采用;应追问“您说的是李明主任吗?”",
- "支持“不限医生”“都可以”等有效值,表示偏好已经解决。",
- "连续两次无法识别时,提供文字、选项或人工服务入口。",
- "重要医疗提示和事务确认必须视觉留痕,不能只通过语音播报。",
- ])
- add_heading(doc, "2.4 通用槽位对象", 2)
- add_code(doc, '''{
- "name": "doctorPreference",
- "rawValue": "李主任",
- "normalizedValue": "DOCTOR_LIMING",
- "source": "VOICE | PROFILE | TOOL | USER_SELECTION",
- "confidence": 0.92,
- "status": "MISSING | INFERRED | CONFIRMED | INVALID | CONFLICTED",
- "required": true,
- "updatedAt": "2026-07-26T10:20:00+08:00"
- }''')
- # 3
- add_heading(doc, "3. 统一完整性与执行门禁", 1)
- add_heading(doc, "3.1 门禁顺序", 2)
- add_numbered(doc, [
- "意图门禁:明确患者要使用挂号、报告解读或中医舌诊中的哪一项服务。",
- "身份门禁:明确当前 hospitalId、operatorId、subjectPatientId;P0 默认本人但字段不得省略。",
- "医疗安全门禁:先处理急诊红旗、危急值、图片/报告不适用等风险。",
- "槽位完整性门禁:所有必填和条件必填槽位均已 CONFIRMED 或具有允许的默认值。",
- "用户确认门禁:执行前展示摘要,患者明确确认。",
- "工具执行门禁:请求版本、结果版本、幂等键和 Mock 场景合法。",
- ])
- add_callout(doc, "禁止", "不得使用“模型认为信息差不多完整”作为执行条件。信息完整性必须由确定性规则检查 missingSlots、conflictedSlots 和 riskDecision。", color=RED, fill="FFF1F1")
- add_heading(doc, "3.2 统一完整性结果", 2)
- add_table(
- doc,
- ["字段", "说明"],
- [
- ("isComplete", "必要字段是否全部解决"),
- ("missingSlots", "仍需追问的槽位,按优先级排序"),
- ("conflictedSlots", "患者多次表达不一致的槽位"),
- ("needsConfirmation", "存在推断值或低置信度值时为 true"),
- ("riskDecision", "ALLOW / BLOCK / HUMAN_REVIEW"),
- ("nextAction", "ASK_SLOT / CONFIRM_SUMMARY / CALL_TOOL / TRANSFER"),
- ],
- [2300, 7060],
- )
- # 4 Registration
- add_heading(doc, "4. 智能分诊挂号:语音与补槽契约", 1)
- add_heading(doc, "4.1 示例主对话", 2)
- add_table(
- doc,
- ["角色", "对话 / 系统行为"],
- [
- ("患者", "“我头疼,帮我挂神经内科明天早上九点的专家号。”"),
- ("智能体", "提取症状、科室、日期、时间、号别;先执行急诊风险检查;发现医生偏好缺失。"),
- ("智能体", "“已记录神经内科、明天上午九点和专家号。请问您想挂哪位医生?”"),
- ("患者", "“李明主任。”"),
- ("智能体", "展示完整摘要;患者确认后才查询号源。若 09:00 无精确号源,展示最接近的可选时段,不得自动替患者选择。"),
- ],
- [1500, 7860],
- )
- add_heading(doc, "4.2 槽位字典", 2)
- add_table(
- doc,
- ["槽位", "来源", "规则", "必填"],
- [
- ("subjectPatientId", "患者上下文", "P0 固定本人;接口必须携带", "是"),
- ("chiefComplaint", "语音", "分诊场景必须;直接找科室时可选", "条件"),
- ("campusId", "语音/默认/追问", "单院区可默认,但摘要中展示", "是"),
- ("departmentCode", "语音/分诊 Tool/选择", "必须映射医院标准科室", "是"),
- ("visitDate", "语音/追问", "将明天、下周一标准化为日期", "是"),
- ("timePreference", "语音/追问", "支持精确时间或上午/下午范围", "是"),
- ("registrationType", "语音/追问", "普通/专家/特需;医院可配置", "是"),
- ("doctorPreference", "语音/追问", "具体医生或 NO_PREFERENCE", "是"),
- ("emergencyDecision", "规则 Tool", "ALLOW / BLOCK / HUMAN_REVIEW", "是"),
- ("userConfirmed", "确认卡片", "锁号/挂号前必须 true", "是"),
- ],
- [2100, 1700, 4200, 1360],
- )
- add_heading(doc, "4.3 追问优先级", 2)
- add_callout(doc, "ORDER", "急诊风险信息 → 科室/症状 → 日期 → 时间 → 号别 → 医生偏好 → 院区 → 完整摘要确认", color=TEAL, fill="E9F7F7")
- add_heading(doc, "4.4 Tool 调用条件", 2)
- add_table(
- doc,
- ["Tool", "允许调用条件", "关键失败结果"],
- [
- ("triage.emergencyScreen", "出现症状或分诊诉求后立即调用", "BLOCK / HUMAN_REVIEW"),
- ("triage.recommendDepartments", "需要分诊且风险允许继续", "LOW_CONFIDENCE / NO_MATCH"),
- ("his.searchSchedules", "所有查询槽位完整并经患者确认", "NO_SLOT / TIMEOUT"),
- ("his.lockRegistrationSlot", "患者选择有效 resultRef + resultVersion + candidateId", "RESULT_EXPIRED / SLOT_UNAVAILABLE"),
- ("his.createRegistration", "锁号有效、就诊人一致、二次确认完成", "DUPLICATE / LOCK_EXPIRED"),
- ("payment.mockPay", "挂号订单已创建", "FAILED / UNKNOWN"),
- ],
- [2200, 4800, 2360],
- )
- add_heading(doc, "4.5 候选结果约束", 2)
- add_bullets(doc, [
- "候选号源必须携带 resultRef、resultVersion、expiresAt 和 candidateId。",
- "患者修改日期、科室、医生或号别后,旧候选立即失效并重新查询。",
- "精确时间无号时只展示替代结果,不得静默改为其他时间。",
- "锁号成功后必须再次展示医生、时间、费用和就诊人并确认。",
- ])
- # 5 Report
- add_heading(doc, "5. 报告解读:语音与补槽契约", 1)
- add_heading(doc, "5.1 入口与对话", 2)
- add_para(doc, "患者可以说“帮我解读刚出的血常规”,也可以从新报告待办进入。智能体先明确报告来源和归属,再判断报告是否为已审核、可解释版本。")
- add_table(
- doc,
- ["槽位", "规则", "必填"],
- [
- ("reportSource", "HOSPITAL_REPORT / IMAGE / PDF / MOCK_SAMPLE", "是"),
- ("reportId 或 fileId", "院内报告使用 reportId;上传使用 fileId", "是"),
- ("reportOwnerConfirmed", "确认属于当前患者;P0 固定本人", "是"),
- ("sourceStatus", "FINAL / PRELIMINARY;P0 仅 FINAL 进入正式解读", "是"),
- ("verificationStatus", "VERIFIED / NEED_REVIEW", "是"),
- ("reportFamily", "LAB / EXAM", "是"),
- ("reportType", "BLOOD_ROUTINE / ULTRASOUND 等", "是"),
- ("consentGranted", "患者同意 AI 辅助解读及结果保存", "是"),
- ("symptoms / medication / history", "根据异常组合条件追问,不应无差别询问", "条件"),
- ],
- [2300, 5500, 1560],
- )
- add_heading(doc, "5.2 报告可用性门禁", 2)
- add_callout(doc, "GATE", "报告存在 + 患者归属确认 + 已授权 + FINAL + VERIFIED + 当前版本有效 + 未命中禁止自动解读规则", color=TEAL, fill="E9F7F7")
- add_bullets(doc, [
- "报告更新、撤回或 sourceHash 变化后,旧解读结果标记 INVALIDATED。",
- "OCR 低置信度的关键指标、单位或参考范围必须回显确认。",
- "危急风险检查先于普通知识检索和解释生成。",
- "优先使用报告原始参考范围,不用 Prompt 中的固定范围覆盖。",
- "检查报告只解释报告文字,不宣称完成原始医学影像诊断。",
- ])
- add_heading(doc, "5.3 输出组件顺序", 2)
- add_numbered(doc, [
- "风险优先提示:需立即联系医生、急诊或无紧急风险。",
- "一句话概览:报告类型、异常数量和主要方向。",
- "异常指标或检查所见卡片。",
- "联合解读、影响因素与不确定性。",
- "下一步行动、建议科室和可向医生提出的问题。",
- "知识与规则来源、报告版本、解读版本和已知限制。",
- ])
- # 6 Tongue
- add_heading(doc, "6. 中医舌诊:语音与补槽契约", 1)
- add_heading(doc, "6.1 主流程", 2)
- add_callout(doc, "FLOW", "语音确认健康诉求 → 服务边界与授权 → 分组语音问答 → 问答完整性检查 → 拍摄指导 → 图片质量检查 → 现有舌诊 MCP → 问答与舌象融合 → 辅助评估结果")
- add_heading(doc, "6.2 槽位字典", 2)
- add_table(
- doc,
- ["槽位组", "核心字段", "规则"],
- [
- ("服务与授权", "healthGoal、consentGranted", "未授权不得上传和分析图片"),
- ("基础信息", "年龄、性别、既往疾病、当前用药", "患者档案优先,患者可纠正"),
- ("寒热", "怕冷/怕热、发热、手脚温度、出汗", "支持“不确定”"),
- ("脾胃", "食欲、腹胀、口干口苦、饮食偏好", "分组追问"),
- ("二便", "大便性状、小便颜色与频率", "允许跳过敏感问题"),
- ("睡眠精神", "入睡、易醒、疲倦、情绪", "分组追问"),
- ("女性专项", "月经、孕期、哺乳期", "条件出现"),
- ("舌象图片", "imageId、qualityStatus、analysisVersion", "质量合格后才能调用 MCP"),
- ],
- [2000, 3500, 3860],
- )
- add_heading(doc, "6.3 图片与结果规则", 2)
- add_bullets(doc, [
- "质量失败必须返回具体原因:模糊、过曝、偏色、舌体不完整或遮挡。",
- "问答进度与图片任务分离保存,重新拍摄不丢失问答。",
- "MCP 必须返回版本、置信度、原始特征、错误码和 unsupported 状态。",
- "问答证据与舌象证据冲突时,应降低置信度并提示中医师进一步确认。",
- "输出使用“体质倾向、证候倾向、辅助评估”,不得表述为确诊或直接开方。",
- ])
- # 7 Gateway
- add_heading(doc, "7. 门户调用智能体协议", 1)
- add_heading(doc, "7.1 请求结构", 2)
- add_code(doc, '''{
- "hospitalId": "H001",
- "operatorId": "USER001",
- "subjectPatientId": "PATIENT001",
- "capabilityCode": "SMART_REGISTRATION",
- "taskId": "TASK001",
- "sessionId": "SESSION001",
- "input": {
- "type": "VOICE_TRANSCRIPT",
- "content": "我头疼,帮我挂神经内科明天早上九点的专家号"
- },
- "context": {
- "channel": "H5",
- "currentPage": "HOME",
- "mock": true
- }
- }''')
- add_heading(doc, "7.2 响应结构", 2)
- add_code(doc, '''{
- "success": true,
- "taskId": "TASK001",
- "taskStatus": "WAITING_USER",
- "currentStep": "ASK_DOCTOR_PREFERENCE",
- "collectedSlots": [],
- "missingSlots": ["doctorPreference"],
- "riskDecision": "ALLOW",
- "ui": [{
- "type": "VOICE_FOLLOW_UP",
- "title": "请问您想挂哪位医生?",
- "quickReplies": ["李明 主任医师", "王佳 副主任医师", "不限医生"]
- }],
- "traceId": "TRACE001"
- }''')
- add_heading(doc, "7.3 能力编码", 2)
- add_table(
- doc,
- ["能力编码", "患者可见名称(医院可配置)", "P0 实现"],
- [
- ("SMART_REGISTRATION", "智能分诊挂号 / 语音挂号", "FastGPT + Mock HIS MCP"),
- ("REPORT_INTERPRETATION", "报告智能解读", "FastGPT + Mock Report MCP + 知识库"),
- ("TCM_TONGUE_ASSESSMENT", "中医舌诊", "FastGPT + 现有舌诊 MCP"),
- ],
- [2700, 3300, 3360],
- )
- # 8 UI
- add_heading(doc, "8. 标准 UI 组件协议", 1)
- add_table(
- doc,
- ["组件", "用途", "必要字段"],
- [
- ("VOICE_INPUT", "主要语音输入", "prompt、status、allowTextFallback"),
- ("TRANSCRIPT", "转写回显与修改", "text、confidence、editable"),
- ("VOICE_FOLLOW_UP", "缺失字段追问", "slotName、prompt、quickReplies"),
- ("SLOT_SUMMARY", "显示信息完整性", "slots、status、confirmAction"),
- ("RISK_ALERT", "急诊/危急/不适用提示", "level、message、actions"),
- ("CANDIDATE_LIST", "科室、医生、号源或报告选择", "resultRef、version、expiresAt、items"),
- ("UPLOAD", "报告或舌象上传", "accept、qualityRequirements"),
- ("CONFIRM_ACTION", "事务或敏感操作确认", "summary、action、expiresAt"),
- ("PROCESSING", "长任务进度", "stage、canCancel、retryPolicy"),
- ("RESULT_SUMMARY", "完成结果与记录入口", "resultId、summary、nextActions"),
- ],
- [2200, 3300, 3860],
- )
- add_callout(doc, "边界", "智能体只能返回标准组件 Schema,不允许返回任意 HTML 控制客户端。语音、文字和卡片入口最终调用同一个 capabilityCode。")
- # 9 MCP
- add_heading(doc, "9. Mock MCP 通用契约", 1)
- add_heading(doc, "9.1 返回格式", 2)
- add_code(doc, '''{
- "success": false,
- "code": "SLOT_UNAVAILABLE",
- "message": "当前号源已不可用",
- "data": {},
- "retryable": true,
- "traceId": "TRACE001",
- "mockScenario": "slot_taken"
- }''')
- add_heading(doc, "9.2 统一错误码", 2)
- add_table(
- doc,
- ["错误码", "含义", "客户端/Workflow 动作"],
- [
- ("INVALID_ARGUMENT", "字段缺失或格式错误", "返回补槽或纠错"),
- ("LOW_CONFIDENCE", "识别或匹配置信度不足", "用户确认或人工处理"),
- ("NO_RESULT", "无科室、报告或号源结果", "提供替代条件"),
- ("RESULT_EXPIRED", "候选结果已过期", "重新查询"),
- ("VERSION_CONFLICT", "结果版本不一致", "废弃旧卡片并重新查询"),
- ("SLOT_UNAVAILABLE", "号源已被占用", "展示其他候选"),
- ("TIMEOUT", "调用超时", "按策略重试,不重复执行副作用"),
- ("DUPLICATE_REQUEST", "重复请求", "返回原业务结果"),
- ("UNAUTHORIZED", "未授权或患者不匹配", "停止流程并重新授权"),
- ("UNSUPPORTED", "当前报告/图片/业务不支持", "说明边界或转人工"),
- ("HUMAN_REVIEW_REQUIRED", "需要人工判断", "携带上下文转人工"),
- ],
- [2500, 3000, 3860],
- )
- add_heading(doc, "9.3 幂等与版本", 2)
- add_bullets(doc, [
- "事务请求必须携带 requestId;相同 requestId 返回同一结果。",
- "所有 Tool 响应必须返回 traceId。",
- "候选结果、报告结果和舌诊分析结果必须有版本。",
- "Mock 场景通过 mockScenario 显式注入,不在 Prompt 中随机决定失败。",
- ])
- # 10 Safety
- add_heading(doc, "10. 医疗安全、隐私与人工转接", 1)
- add_table(
- doc,
- ["场景", "强制动作"],
- [
- ("胸痛、明显呼吸困难、意识异常等红旗", "中止普通流程;急诊、120 或现场就医提示"),
- ("报告危急规则命中", "先展示风险行动,不用长篇解释稀释提示"),
- ("信息矛盾或连续识别失败", "转人工或提供传统页面入口"),
- ("患者身份/报告归属不一致", "停止处理,不展示或保存结果"),
- ("舌象图片质量不足或不适用", "说明原因并重新采集,不生成结论"),
- ("Tool 状态 UNKNOWN", "禁止宣称成功;进入查询状态或人工处理"),
- ],
- [3300, 6060],
- )
- add_bullets(doc, [
- "P0 所有患者数据均为 Mock,并在页面和演示脚本中明确标识。",
- "日志不记录不必要的完整身份证号、电话号码、报告原图和原始语音。",
- "人工转接应携带已收集槽位、风险结果、失败原因和 traceId,避免患者重复描述。",
- "患者可随时取消当前任务;取消后不得继续执行有副作用的 Tool。",
- ])
- # 11 Acceptance
- add_heading(doc, "11. P0 金标用例与验收", 1)
- add_heading(doc, "11.1 挂号", 2)
- add_table(
- doc,
- ["用例", "输入", "预期"],
- [
- ("完整但缺医生", "头疼;神经内科;明天 9 点;专家号", "只追问医生;完成后才能查询号源"),
- ("不限医生", "明天下午挂消化内科,医生都可以", "doctorPreference=NO_PREFERENCE,不重复追问"),
- ("纠正时间", "明天上午;随后改为后天下午", "旧值被替换,旧候选失效"),
- ("急诊红旗", "胸痛并呼吸困难,想挂心内科", "中止普通挂号并急诊引导"),
- ("号源失效", "选择已过期 candidateId", "提示重新查询,不进入挂号"),
- ],
- [1800, 3500, 4060],
- )
- add_heading(doc, "11.2 报告与舌诊", 2)
- add_table(
- doc,
- ["智能体", "用例", "预期"],
- [
- ("报告", "选择已审核血常规并授权", "完成风险检查、结构化解读与记录"),
- ("报告", "报告为 PRELIMINARY 或 NEED_REVIEW", "不生成正式解读;说明原因"),
- ("报告", "报告版本更新", "旧解读结果失效"),
- ("舌诊", "问答完成、图片模糊", "保留问答,只要求重拍"),
- ("舌诊", "问答与舌象证据冲突", "降低置信度并提示医生确认"),
- ("舌诊", "MCP 返回 UNSUPPORTED", "不生成证候结论,说明边界"),
- ],
- [1500, 3700, 4160],
- )
- add_heading(doc, "11.3 通过标准", 2)
- add_bullets(doc, [
- "所有金标用例可重复运行,结果稳定且 traceId 可查询。",
- "任何必填槽位缺失时,不得错误进入 Tool 执行。",
- "任何医疗安全 BLOCK 场景不得继续普通业务路径。",
- "FastGPT、Prompt、知识库或 MCP 修改后必须重新回归。",
- "演示环境可一键重置到固定 Mock 初始状态。",
- ])
- # 12 Delivery
- add_heading(doc, "12. 实施交付清单", 1)
- add_table(
- doc,
- ["交付物", "负责人", "完成证据"],
- [
- ("三智能体槽位 Schema", "FastGPT / 后端", "JSON Schema + 示例"),
- ("统一门户调用协议", "客户端 / Gateway", "请求响应样例"),
- ("标准 UI Schema", "客户端 / 产品", "组件清单 + 渲染 Demo"),
- ("Mock MCP 契约", "后端", "接口、错误码、Mock 场景"),
- ("三个 FastGPT Workflow", "智能体开发", "导出版本 + 节点说明"),
- ("金标测试集", "产品 / 测试", "输入、预期槽位、预期路径"),
- ("发布清单", "技术负责人", "版本、模型、Prompt、知识库、MCP"),
- ("演示脚本", "演示负责人", "正常、失败、安全路径"),
- ],
- [3000, 2000, 4360],
- )
- add_heading(doc, "12.1 发布清单", 2)
- add_table(
- doc,
- ["版本项", "记录内容"],
- [
- ("Portal", "客户端版本、能力配置版本"),
- ("Agent", "capabilityCode、FastGPT App、Workflow 版本"),
- ("Model / Prompt", "模型名称、参数、Prompt 版本"),
- ("Knowledge", "知识库名称、版本、审核人和更新时间"),
- ("Tool / MCP", "接口版本、Mock 数据集、错误场景版本"),
- ("Safety", "急诊/危急值/不适用规则版本"),
- ("Test", "回归测试版本、通过率、已知限制"),
- ],
- [2600, 6760],
- )
- # Appendix
- add_heading(doc, "附录 A:FastGPT 画布落地建议", 1)
- add_callout(doc, "分区", "INPUT_* → NLP_* → SAFETY_* → SLOT_* → DECISION_* → TOOL_* → OUTPUT_*")
- add_table(
- doc,
- ["节点", "输入", "输出"],
- [
- ("INPUT_VOICE_TRANSCRIPT", "语音转写、患者上下文", "rawUtterance"),
- ("NLP_ROUTE_CAPABILITY", "rawUtterance", "capabilityCode、confidence"),
- ("NLP_EXTRACT_SLOTS", "utterance + existingSlots", "slotUpdates"),
- ("SAFETY_GATE", "结构化症状/报告/图片状态", "riskDecision"),
- ("SLOT_VALIDATE", "slot schema + collectedSlots", "missing/conflicted/complete"),
- ("DECISION_NEXT_ACTION", "完整性与风险结果", "ASK / CONFIRM / TOOL / TRANSFER"),
- ("TOOL_CALL", "标准业务参数", "toolResult"),
- ("OUTPUT_UI_SCHEMA", "任务状态与结果", "标准 UI 组件"),
- ],
- [2900, 3200, 3260],
- )
- add_heading(doc, "附录 B:统一任务上下文", 1)
- add_code(doc, '''{
- "taskId": "TASK001",
- "sessionId": "SESSION001",
- "hospitalId": "H001",
- "operatorId": "USER001",
- "subjectPatientId": "PATIENT001",
- "capabilityCode": "SMART_REGISTRATION",
- "taskStatus": "WAITING_USER",
- "currentStep": "ASK_DOCTOR_PREFERENCE",
- "collectedSlots": {},
- "missingSlots": [],
- "conflictedSlots": [],
- "riskDecision": "ALLOW",
- "activeResultRef": null,
- "toolResults": [],
- "evidence": [],
- "mock": true,
- "traceId": "TRACE001"
- }''')
- add_callout(doc, "最终原则", "语音负责表达,智能体负责补槽,规则负责门禁,卡片负责确认,MCP 负责确定性能力,任务记录负责恢复与追溯。", color=TEAL, fill="E9F7F7")
- doc.core_properties.title = "医梦 P0 三智能体语音交互、补槽字段与调用契约 V1.0"
- doc.core_properties.subject = "医梦患者智能服务门户(Adjutant)执行级附件"
- doc.core_properties.author = "医梦开发技术中心"
- doc.core_properties.keywords = "P0, FastGPT, 语音交互, 补槽, MCP, 挂号, 报告解读, 中医舌诊"
- doc.save(OUT)
- print(OUT)
|