"""冻结并校验运行配置,避免历史任务被后续 Agent/Provider 编辑污染。""" from __future__ import annotations import hashlib import json from sqlmodel import Session, select from ..models import Agent, AgentPromptTemplate, AppUser, ModelCatalog, ModelProvider, now from .coordinator import _provider_model, provider_fingerprint from .skills import agent_skills from .memory import memory_candidates from ..digital_persona import freeze_persona from .default_composer import resolve_composer_agent def _digest(value: object) -> str: raw = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str) return hashlib.sha256(raw.encode("utf-8")).hexdigest() def freeze_agent_snapshot(session: Session, agent: Agent, visited: set[int] | None = None, user: AppUser | None = None, allow_agent_scope: bool = False, allowed_agent_ids: set[int] | None = None) -> dict: """返回无密钥的不可变 Agent 配置,嵌套融合 Agent 也一并冻结。""" visited = visited or set() if agent.id in visited: return {"id": agent.id, "name": agent.name, "cycle": True} visited.add(agent.id or 0) provider: ModelProvider | None = None model = agent.model_name catalog: ModelCatalog | None = session.get(ModelCatalog, agent.model_catalog_id) if agent.model_catalog_id else None try: provider, model = _provider_model(session, agent) except Exception: provider = session.get(ModelProvider, agent.model_provider_id) if agent.model_provider_id else None skills = [{"id": skill.id, "name": skill.name, "version": skill.version, "instructions": skill.instructions} for skill in agent_skills(session, agent.id or 0)] memories = memory_candidates(session, agent, user, allow_agent_scope=allow_agent_scope, allowed_agent_ids=allowed_agent_ids) # 人物表达统一由已绑定的数字人提供。旧模板仅保留身份元数据用于历史 # 审计和兼容快照,正文不会注入运行时,避免两套表达规则互相覆盖。 template = session.get(AgentPromptTemplate, agent.prompt_template_id) if agent.prompt_template_id else None template_snapshot = {} if template and template.enabled: # 保留完整历史快照供审计/旧任务回放;当前 runner 会忽略其表达字段, # 新任务的文风统一从数字人快照注入。 template_snapshot = { "id": template.id, "name": template.name, "version": template.version, "role_prompt": template.role_prompt, "task_template": template.task_template, "constraints": list(template.constraints or []), "examples": list(template.examples or []), "output_schema": dict(template.output_schema or {}), "style": template.style, } fusions = [] for fusion_id in agent.fusion_agent_ids or []: fused = session.get(Agent, fusion_id) if fused: fusions.append(freeze_agent_snapshot(session, fused, visited.copy(), user=user, allow_agent_scope=allow_agent_scope, allowed_agent_ids=allowed_agent_ids)) snapshot = { "id": agent.id, "name": agent.name, "role": agent.role, "organization_id": agent.organization_id, "owner_user_id": agent.owner_user_id, **freeze_persona(session, agent, user), "rules": agent.rules, "rules_version": agent.rules_version, "rules_hash": hashlib.sha256((agent.rules or "").encode("utf-8")).hexdigest(), "kb_ids": list(agent.kb_ids or []), "kb_category_ids": dict(getattr(agent, "kb_category_ids", {}) or {}), "allowed_tools": list(agent.allowed_tools or []), "provider_id": provider.id if provider else agent.model_provider_id, "provider_name": provider.name if provider else "", "provider_base_url": provider.base_url if provider else "", "provider_fingerprint": provider_fingerprint(provider, model) if provider else "", "model": model, "model_catalog_id": catalog.id if catalog else agent.model_catalog_id, "model_catalog": { "id": catalog.id, "name": catalog.model_name, "display_name": catalog.display_name, "capabilities": list(catalog.capabilities or []), "capability_status": catalog.capability_status, "rate_version": catalog.rate_version, } if catalog else {}, "skills": skills, "memories": memories, "fusions": fusions, "prompt_template": template_snapshot, } snapshot["snapshot_hash"] = _digest(snapshot) return snapshot def freeze_stage_snapshot(session: Session) -> dict: """一次运行中 QC 和组合阶段使用的完整快照。""" qc: dict[str, dict] = {} for gate, name in { "structure": "结构质检", "content": "内容质检", "style": "风格尺度质检", "cross": "跨稿质检", }.items(): agent = session.exec(select(Agent).where(Agent.name == name)).first() if agent: qc[gate] = freeze_agent_snapshot(session, agent) composer = resolve_composer_agent(session) payload = { "schema_version": 1, "captured_at": now(), "qc": qc, "composer": freeze_agent_snapshot(session, composer), } payload["snapshot_hash"] = _digest(payload) return payload def agent_from_snapshot(snapshot: dict) -> Agent: """把冻结的数据转为运行时 Agent,不再读取当前的规则或技能绑定。""" agent = Agent( id=int(snapshot.get("id") or 0), name=str(snapshot.get("name") or "冻结 Agent"), role=str(snapshot.get("role") or "service"), rules=str(snapshot.get("rules") or ""), rules_version=int(snapshot.get("rules_version") or 1), organization_id=int(snapshot.get("organization_id") or 1), owner_user_id=int(snapshot.get("owner_user_id") or 0), digital_human_id=snapshot.get("digital_human_id"), persona_json=dict(snapshot.get("persona_json") or {}), runtime_snapshot={"primary": snapshot}, kb_ids=list(snapshot.get("kb_ids") or []), kb_category_ids=dict(snapshot.get("kb_category_ids") or {}), status="active", allowed_tools=list(snapshot.get("allowed_tools") or []), ) object.__setattr__(agent, "_frozen_persona_snapshot", snapshot) return agent def skills_prompt_from_snapshot(snapshot: dict) -> str: skills = list(snapshot.get("skills") or []) if not skills: return "" lines = ["## 已冻结技能(按下列规则执行)"] for skill in skills: lines.append(f"### {skill.get('name') or '未命名技能'} v{skill.get('version') or 1}") lines.append(str(skill.get("instructions") or "")) return "\n".join(lines) def resolve_snapshot_provider(session: Session, snapshot: dict) -> tuple[ModelProvider, str]: """只允许以原 Provider 和模型续跑;配置漂移必须由人工创建新运行。""" provider_id = int(snapshot.get("provider_id") or 0) model = str(snapshot.get("model") or "") provider = session.get(ModelProvider, provider_id) if not provider or not provider.enabled or not model: raise ValueError("冻结的模型配置已不可用,请创建新运行后重试") expected = str(snapshot.get("provider_fingerprint") or "") actual = provider_fingerprint(provider, model) if expected and expected != actual: raise ValueError("Provider 配置已变更,已阻止使用新配置覆盖历史运行") return provider, model