| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277 |
- """Build the concise P0 conversation-first interaction overview."""
- from __future__ import annotations
- import copy
- import json
- import os
- from pathlib import Path
- ROOT = Path(__file__).resolve().parent
- TEMPLATE = Path(
- os.environ.get(
- "EXCALIDRAW_TEMPLATE",
- ROOT / "templates" / "relationship-template.excalidraw",
- )
- )
- OUTPUT = ROOT / "医梦患者智能服务门户_P0对话式三智能体总交互流程.excalidraw"
- if not TEMPLATE.exists():
- raise SystemExit(
- "未找到 Excalidraw 模板。请通过 EXCALIDRAW_TEMPLATE 指定 "
- "relationship-template.excalidraw。"
- )
- source = json.loads(TEMPLATE.read_text(encoding="utf-8"))
- rect_template = next(element for element in source["elements"] if element["type"] == "rectangle")
- bound_text_template = next(element for element in source["elements"] if element["type"] == "text")
- free_text_template = next(element for element in source["elements"] if element["type"] == "text")
- arrow_template = next(element for element in source["elements"] if element["type"] == "arrow")
- sequence = 5000
- def next_value() -> int:
- global sequence
- sequence += 1
- return sequence
- def box(
- x: int,
- y: int,
- width: int,
- height: int,
- text: str,
- *,
- fill: str,
- stroke: str,
- color: str = "#16133a",
- size: int = 18,
- ) -> list[dict]:
- rect = copy.deepcopy(rect_template)
- label = copy.deepcopy(bound_text_template)
- rect_id = f"box-{next_value()}"
- text_id = f"text-{next_value()}"
- group_id = f"group-{next_value()}"
- seed = next_value()
- rect.update(
- {
- "id": rect_id,
- "x": x,
- "y": y,
- "width": width,
- "height": height,
- "strokeColor": stroke,
- "backgroundColor": fill,
- "groupIds": [group_id],
- "boundElements": [{"type": "text", "id": text_id}],
- "seed": seed,
- "versionNonce": seed + 1,
- }
- )
- line_count = text.count("\n") + 1
- text_height = min(int(line_count * size * 1.4 + 8), height - 16)
- label.update(
- {
- "id": text_id,
- "x": x + 14,
- "y": y + (height - text_height) / 2,
- "width": width - 28,
- "height": text_height,
- "text": text,
- "fontSize": size,
- "fontFamily": 5,
- "strokeColor": color,
- "textAlign": "center",
- "verticalAlign": "middle",
- "containerId": rect_id,
- "groupIds": [group_id],
- "seed": seed + 2,
- "versionNonce": seed + 3,
- }
- )
- return [rect, label]
- def free_text(x: int, y: int, width: int, height: int, text: str, *, size: int, color: str) -> dict:
- label = copy.deepcopy(free_text_template)
- seed = next_value()
- label.update(
- {
- "id": f"label-{next_value()}",
- "x": x,
- "y": y,
- "width": width,
- "height": height,
- "text": text,
- "fontSize": size,
- "fontFamily": 5,
- "strokeColor": color,
- "textAlign": "left",
- "containerId": None,
- "groupIds": [],
- "seed": seed,
- "versionNonce": seed + 1,
- }
- )
- return label
- def arrow(x: int, y: int, points: list[list[int]], *, color: str, dashed: bool = False) -> dict:
- element = copy.deepcopy(arrow_template)
- xs = [point[0] for point in points]
- ys = [point[1] for point in points]
- seed = next_value()
- element.update(
- {
- "id": f"arrow-{next_value()}",
- "x": x,
- "y": y,
- "width": max(xs) - min(xs),
- "height": max(ys) - min(ys),
- "points": points,
- "strokeColor": color,
- "strokeWidth": 3,
- "strokeStyle": "dashed" if dashed else "solid",
- "startBinding": None,
- "endBinding": None,
- "groupIds": [],
- "seed": seed,
- "versionNonce": seed + 1,
- }
- )
- return element
- elements: list[dict | list[dict]] = [
- free_text(
- 80,
- 40,
- 2200,
- 60,
- "医梦患者智能服务门户|P0 对话式三智能体总交互流程",
- size=34,
- color="#21147d",
- ),
- free_text(
- 80,
- 100,
- 2200,
- 40,
- "单医院 · 固定 Mock 患者 · 语音优先 · 三个智能体共用一个对话任务壳",
- size=18,
- color="#69708a",
- ),
- box(
- 80,
- 230,
- 360,
- 560,
- "门户入口\n\n统一语音入口\n不知道使用哪个服务\n\n三项服务卡片\n明确选择挂号、报告或舌诊\n\n患者状态与待办\n继续报告或舌诊任务",
- fill="#f1f0ff",
- stroke="#2b1f99",
- color="#21147d",
- size=19,
- ),
- box(
- 570,
- 230,
- 480,
- 560,
- "统一对话式任务壳\n\n保留患者原话与助手回复\n\n语音主输入\n文字、快捷选项、上传为辅\n\n智能体连续追问补齐信息\n\n只在必要时出现\n号源选择 / 风险提醒 / 操作确认 / 结果\n\n三个智能体独立保存任务状态",
- fill="#ffffff",
- stroke="#2b1f99",
- color="#21147d",
- size=19,
- ),
- box(
- 1210,
- 170,
- 620,
- 240,
- "挂号智能体\n\n语音描述完整诉求\n→ 只追问缺失医生\n→ 查询并选择 Mock 号源\n→ 确认 Mock 挂号\n\n安全支线:胸痛 + 呼吸困难 → 中止普通挂号",
- fill="#f1f0ff",
- stroke="#2b1f99",
- color="#21147d",
- size=18,
- ),
- box(
- 1210,
- 480,
- 620,
- 240,
- "报告解读智能体\n\n对话内上传报告\n→ OCR 与指标分析进度\n→ 结构化 Markdown 解读\n→ 风险提醒 + 异常指标表 + 行动建议",
- fill="#eef7ff",
- stroke="#4f7fdb",
- color="#254b8d",
- size=18,
- ),
- box(
- 1210,
- 790,
- 620,
- 240,
- "中医舌诊智能体\n\n对话内完成体质与症候问答\n→ 上传舌象\n→ 调用现有舌诊 MCP\n→ 返回问答 + 舌象综合结果",
- fill="#fff4ea",
- stroke="#d8874c",
- color="#8a4d25",
- size=18,
- ),
- box(
- 2010,
- 390,
- 430,
- 430,
- "统一结果回流\n\n挂号:Mock 预约结果\n\n报告:Markdown 解读与风险提醒\n\n舌诊:体质 / 证候倾向与健康建议\n\n完成结果进入健康记录\n未完成任务回到首页待办",
- fill="#e9fbfc",
- stroke="#2fc9d2",
- color="#115e64",
- size=19,
- ),
- arrow(440, 510, [[0, 0], [130, 0]], color="#2b1f99"),
- arrow(1050, 510, [[0, 0], [80, 0], [80, -220], [160, -220]], color="#2b1f99"),
- arrow(1050, 510, [[0, 0], [160, 90]], color="#4f7fdb"),
- arrow(1050, 510, [[0, 0], [80, 0], [80, 400], [160, 400]], color="#d8874c"),
- arrow(1830, 290, [[0, 0], [90, 0], [90, 315], [180, 315]], color="#2b1f99"),
- arrow(1830, 600, [[0, 0], [180, 5]], color="#4f7fdb"),
- arrow(1830, 910, [[0, 0], [90, 0], [90, -305], [180, -305]], color="#d8874c"),
- arrow(2225, 820, [[0, 0], [0, 290], [-1965, 290], [-1965, -320]], color="#2fc9d2", dashed=True),
- ]
- flattened: list[dict] = []
- for element in elements:
- if isinstance(element, (tuple, list)):
- flattened.extend(element)
- else:
- flattened.append(element)
- result = {
- "type": "excalidraw",
- "version": 2,
- "source": "https://excalidraw.com",
- "elements": flattened,
- "appState": {"viewBackgroundColor": "#f7fbff", "gridSize": 20},
- "files": {},
- }
- assert all(isinstance(element, dict) for element in flattened)
- assert len({element["id"] for element in flattened}) == len(flattened)
- assert all(
- element.get("fontFamily") == 5
- for element in flattened
- if element.get("type") == "text"
- )
- for element in flattened:
- if element.get("type") == "rectangle":
- assert element.get("boundElements")
- text_id = element["boundElements"][0]["id"]
- child = next(item for item in flattened if item["id"] == text_id)
- assert child.get("containerId") == element["id"]
- assert child.get("groupIds") == element.get("groupIds")
- OUTPUT.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
- json.loads(OUTPUT.read_text(encoding="utf-8"))
- print(OUTPUT)
|