| """技能契约 PharmaSkill 与三层数据载体类型。 |
| |
| 本模块定义「底座 + 可插拔 Skill」平台的核心**类型契约**,用类型签名把 |
| 三层职责锁死(需求 1.4、2.1): |
| |
| - Layer 1 ``extract``:可用 LLM,仅产出结构化数据(``ExtractedData``)。 |
| - Layer 2 ``compute``:**纯 Python**,方法签名不接收任何服务 / LLM 句柄, |
| 从源头杜绝幻觉计算(需求 2.1 的硬保证)。 |
| - Layer 3 ``explain``:LLM 仅将只读计算结果转写为专业文本,禁止任何数值计算。 |
| |
| 数据载体一律采用 ``dataclass``,确保三层之间传递的是结构化只读对象。 |
| |
| 设计要点(对应 design.md「1. 技能契约」):``compute(self, data)`` **不传 ``svc``**, |
| 任何 Skill 作者即便想在计算阶段调用 LLM,也没有句柄可用;审计追踪通过返回 |
| ``ComputeResult.trace`` 由底座统一落库,而非在计算内部调用服务。 |
| """ |
|
|
| from __future__ import annotations |
|
|
| from abc import ABC, abstractmethod |
| from dataclasses import dataclass, field |
| from enum import Enum |
| from typing import TYPE_CHECKING |
|
|
| if TYPE_CHECKING: |
| |
| |
| from .services import Services |
|
|
|
|
| class InputKind(str, Enum): |
| """Skill 声明可处理的输入类型,供底座路由匹配(需求 1.5)。""" |
|
|
| FILE = "file" |
| SMILES = "smiles" |
| EXCIPIENT = "excipient" |
| TEXT = "text" |
|
|
|
|
| @dataclass |
| class SkillMeta: |
| """Skill 元数据:身份、展示与路由信息(需求 1.4)。""" |
|
|
| id: str |
| display_name: str |
| description: str |
| version: str |
| input_kinds: set[InputKind] |
| icon: str = "💊" |
| order: int = 100 |
| |
| |
| |
| |
| two_phase: bool = True |
|
|
|
|
| @dataclass |
| class RawInput: |
| """底座收集的原始输入(未结构化)。""" |
|
|
| smiles: str = "" |
| excipient: str = "" |
| goal: str = "" |
| files: list = field(default_factory=list) |
| extra: dict = field(default_factory=dict) |
|
|
|
|
| @dataclass |
| class ExtractedData: |
| """Layer1 输出:结构化数据。 |
| |
| ``payload`` 由各 Skill 定义其 schema;``method`` 标注提取方式。 |
| """ |
|
|
| payload: dict |
| notes: str = "" |
| method: str = "" |
|
|
|
|
| @dataclass |
| class ComputeResult: |
| """Layer2 输出:只读计算结果 + 图表数据 + 审计追踪。 |
| |
| - ``summary``:核心计算结论(k 值、R²、外推预测值、货架期等)。 |
| - ``figures``:``chart_service`` 可渲染的结构化图表数据。 |
| - ``trace``:计算追踪条目,由底座统一落审计库(需求 2 / 9.5)。 |
| - ``can_proceed`` / ``refusal``:数据不足等情形的优雅拒绝(需求 8.3)。 |
| """ |
|
|
| summary: dict |
| figures: dict = field(default_factory=dict) |
| trace: list = field(default_factory=list) |
| can_proceed: bool = True |
| refusal: dict | None = None |
|
|
|
|
| @dataclass |
| class ReportSections: |
| """Layer3 输出:各段落文本(HTML 片段)。""" |
|
|
| sections: dict |
| html: str = "" |
|
|
|
|
| class PharmaSkill(ABC): |
| """统一技能契约。 |
| |
| 每个分析功能作为实现本契约的自包含 Skill 存在。底座固定流水线为 |
| ``extract → compute → explain → report``。 |
| |
| **关键架构约束(需求 2.1)**:``compute(self, data)`` 的签名**不含** |
| ``svc`` 等服务 / LLM 句柄——这是从类型层面阻断「大模型幻觉计算」的硬保证。 |
| """ |
|
|
| |
| meta: SkillMeta |
|
|
| @abstractmethod |
| def render_inputs(self, st_ctx) -> RawInput: |
| """在 Streamlit 中渲染本 Skill 的输入面板,返回 ``RawInput``。""" |
|
|
| @abstractmethod |
| def extract(self, raw: RawInput, svc: "Services") -> ExtractedData: |
| """Layer1:可用 ``svc.llm`` 做提取,仅产出结构化数据。""" |
|
|
| @abstractmethod |
| def compute(self, data: ExtractedData) -> ComputeResult: |
| """Layer2:纯 Python。 |
| |
| 注意签名无 ``svc``——禁止 LLM 介入计算。任何数值结论必须由确定性 |
| Python 计算得出,审计追踪通过返回 ``ComputeResult.trace`` 暴露给底座。 |
| """ |
|
|
| @abstractmethod |
| def explain(self, result: ComputeResult, svc: "Services") -> ReportSections: |
| """Layer3:用 ``svc.llm`` 将只读结果转写为文字,禁止改数值。""" |
|
|
| def can_handle(self, raw: RawInput) -> float: |
| """返回 0..1 的可处理置信度,供 Router 兜底打分(可选覆写)。""" |
| return 0.0 |
|
|
| def _t(self, key: str, default: str = "") -> str: |
| """取本 Skill 输入面板文案:优先底座注入的绑定翻译器(随语言切换), |
| 否则回退到 ``default``(中文兜底)。 |
| |
| 底座在渲染前通过 ``app._safe_render_inputs`` 注入 ``self._i18n_t`` |
| (一个接收局部键、返回本地化文案的可调用对象,需求 3 / 17.4)。无注入 |
| (如无 GUI 的纯逻辑单测)时,返回 ``default`` 以保持原有行为,不抛异常。 |
| """ |
| translator = getattr(self, "_i18n_t", None) |
| if callable(translator): |
| try: |
| text = translator(key) |
| |
| if text and text != key and not str(text).endswith(f".{key}"): |
| return text |
| except Exception: |
| pass |
| return default |
|
|
|
|
| __all__ = [ |
| "InputKind", |
| "SkillMeta", |
| "RawInput", |
| "ExtractedData", |
| "ComputeResult", |
| "ReportSections", |
| "PharmaSkill", |
| ] |
|
|