feat(server): add workbench API, DSH planner worker and RAG pipeline

This commit is contained in:
yi
2026-09-16 19:06:20 +08:00
parent 72fc5b47c7
commit d4b5b3f43a
122 changed files with 31906 additions and 0 deletions
+149
View File
@@ -0,0 +1,149 @@
"""冻结并校验运行配置,避免历史任务被后续 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