| """分析编排器 ``AnalysisOrchestrator``(intent-understanding-layer 任务 11)。 |
| |
| 协调「理解 → 确认 → 分析」两阶段流程(design.md「4. AnalysisOrchestrator」): |
| |
| - Phase A::meth:`build_task_sheet` 调用理解层产出《分析任务单》(不触达 ``compute``)。 |
| - Phase B::meth:`run_confirmed` 接收**用户已确认**的任务单,经 :meth:`Router.route_from_sheet` |
| 路由后,把任务单挂到 ``RawInput.extra["task_sheet"]`` 交由固定 ``Pipeline`` 执行。 |
| |
| 合规保证(Property 5 / 需求 1.3、5.4、6.2):``compute`` 只能经 :meth:`run_confirmed` |
| 触达,而后者要求传入一份非澄清态任务单;澄清态 / 路由失败时**短路返回**澄清说明, |
| 绝不进入流水线。审计在确认执行时记录(需求 8.2 / 9.4)。 |
| """ |
|
|
| from __future__ import annotations |
|
|
| import logging |
| from typing import TYPE_CHECKING, Any, Optional |
|
|
| from .pipeline import Pipeline |
| from .router import Router |
| from .skill_base import RawInput, ReportSections |
| from .task_sheet import AnalysisTaskSheet |
| from .understanding import UnderstandingLayer |
|
|
| if TYPE_CHECKING: |
| from .registry import SkillRegistry |
| from .services import Services |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| class AnalysisOrchestrator: |
| """两阶段分析编排:理解(Phase A)→ 用户确认 → 分析(Phase B)。""" |
|
|
| def __init__(self, registry: "SkillRegistry", svc: "Services") -> None: |
| self.registry = registry |
| self.svc = svc |
| self.router = Router(registry, svc) |
|
|
| |
| |
| |
| def build_task_sheet(self, raw: RawInput) -> AnalysisTaskSheet: |
| """运行理解层产出《分析任务单》(不进行任何分析数值计算,Property 4)。""" |
| return UnderstandingLayer(self.svc).understand(raw) |
|
|
| |
| |
| |
| def run_confirmed( |
| self, |
| sheet: AnalysisTaskSheet, |
| raw: RawInput, |
| *, |
| pipeline: Optional[Pipeline] = None, |
| user: str = "anonymous", |
| ) -> ReportSections: |
| """执行已确认任务单对应的分析。 |
| |
| 前置:``sheet`` 必须为**非澄清态**(澄清态直接返回澄清说明,Property 5)。 |
| 路由失败(需用户选择)同样短路返回,不进入流水线。 |
| """ |
| |
| if sheet is None or sheet.needs_clarification(): |
| return self._clarification_sections(sheet) |
|
|
| decision = self.router.route_from_sheet(sheet) |
| if decision.needs_user_choice or decision.skill is None: |
| return ReportSections( |
| sections={ |
| "clarification": { |
| "reason": "需要用户选择分析功能", |
| "detail": decision.reason, |
| } |
| } |
| ) |
|
|
| raw2 = self._attach_confirmed_payload(raw, sheet) |
| self._audit_confirm(user, decision.skill.meta.id, sheet) |
|
|
| pipeline = pipeline or Pipeline() |
| return pipeline.run(decision.skill, raw2, self.svc) |
|
|
| |
| @staticmethod |
| def _attach_confirmed_payload(raw: RawInput, sheet: AnalysisTaskSheet) -> RawInput: |
| """把已确认任务单写入 ``RawInput.extra["task_sheet"]``(不改契约)。""" |
| extra = dict(getattr(raw, "extra", None) or {}) |
| extra["task_sheet"] = sheet.to_dict() |
| return RawInput( |
| smiles=getattr(raw, "smiles", "") or "", |
| excipient=getattr(raw, "excipient", "") or "", |
| goal=getattr(raw, "goal", "") or "", |
| files=list(getattr(raw, "files", None) or []), |
| extra=extra, |
| ) |
|
|
| @staticmethod |
| def _clarification_sections(sheet: Optional[AnalysisTaskSheet]) -> ReportSections: |
| clar = getattr(sheet, "clarification", None) if sheet else None |
| return ReportSections( |
| sections={ |
| "clarification": { |
| "reason_code": getattr(clar, "reason_code", "") if clar else "", |
| "message_key": getattr(clar, "message_key", "") if clar else "", |
| "options": list(getattr(clar, "options", []) or []) if clar else [], |
| } |
| } |
| ) |
|
|
| def _audit_confirm(self, user: str, skill_id: str, sheet: AnalysisTaskSheet) -> None: |
| audit = getattr(self.svc, "audit", None) |
| recorder = getattr(audit, "record_analysis", None) or getattr(audit, "record_event", None) |
| if callable(recorder): |
| try: |
| recorder(user or "anonymous", skill_id, step="confirmed_task_sheet") |
| except TypeError: |
| try: |
| recorder("confirm", skill_id, sheet.intent.value) |
| except Exception: |
| logger.debug("编排器确认审计失败(已忽略)。", exc_info=True) |
| except Exception: |
| logger.debug("编排器确认审计失败(已忽略)。", exc_info=True) |
|
|
|
|
| __all__ = ["AnalysisOrchestrator"] |
|
|