feat(server): add workbench API, DSH planner worker and RAG pipeline
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
"""提示模板渲染和结构化输出校验。
|
||||
|
||||
模板只负责组织输入,Agent rules 和 Runtime 硬规则仍由系统注入,避免
|
||||
用户模板覆盖知识来源、消费锁和运行状态约束。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
|
||||
PROMPT_PLACEHOLDERS = {
|
||||
"agent_name", "agent_role", "title", "content_tag", "time_range",
|
||||
"mindmap_node", "product_ref", "next_context", "previous_context",
|
||||
"qc_block", "anchor_schema",
|
||||
}
|
||||
|
||||
DEFAULT_ROLE_PROMPT = "你是{agent_name},负责{agent_role}类型的内容任务。"
|
||||
DEFAULT_TASK_TEMPLATE = """请完成以下任务:
|
||||
标题:{title}
|
||||
内容标签:{content_tag}
|
||||
时间范围:{time_range}
|
||||
导图节点:{mindmap_node}
|
||||
产品节点:{product_ref}
|
||||
"""
|
||||
DEFAULT_CONSTRAINTS = [
|
||||
"只围绕本段任务和已提供材料作答,缺少事实时标记待确认。",
|
||||
"输出自然口语,先讲清楚为什么,再给出本段要求的内容。",
|
||||
"正文后输出完整锚点卡 JSON,字段必须与 Schema 一致。",
|
||||
]
|
||||
DEFAULT_OUTPUT_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"required": ["core_concepts", "techniques", "products_ref", "golden_lines", "emotion_tone", "ending_hook"],
|
||||
"properties": {
|
||||
"core_concepts": {"type": "string"},
|
||||
"techniques": {"type": "string"},
|
||||
"products_ref": {"type": "string"},
|
||||
"golden_lines": {"type": "string"},
|
||||
"emotion_tone": {"type": "string"},
|
||||
"ending_hook": {"type": "string"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _string(value: object, limit: int = 20000) -> str:
|
||||
return str(value or "").strip()[:limit]
|
||||
|
||||
|
||||
def normalize_template_payload(payload: dict[str, Any], *, partial: bool = False) -> dict[str, Any]:
|
||||
"""校验并规范模板输入,供 API 和 seed 共用。"""
|
||||
result: dict[str, Any] = {}
|
||||
if not partial or "name" in payload:
|
||||
name = _string(payload.get("name"), 120)
|
||||
if not name:
|
||||
raise ValueError("提示模板名称不能为空")
|
||||
result["name"] = name
|
||||
for key, limit in (("desc", 500), ("role_prompt", 6000), ("task_template", 12000), ("style", 6000)):
|
||||
if key in payload:
|
||||
result[key] = _string(payload.get(key), limit)
|
||||
if "constraints" in payload:
|
||||
constraints = payload.get("constraints")
|
||||
if not isinstance(constraints, list) or any(not _string(item, 1000) for item in constraints):
|
||||
raise ValueError("提示模板约束必须是非空字符串列表")
|
||||
result["constraints"] = [_string(item, 1000) for item in constraints[:30]]
|
||||
if "examples" in payload:
|
||||
examples = payload.get("examples")
|
||||
if not isinstance(examples, list):
|
||||
raise ValueError("提示模板示例必须是列表")
|
||||
normalized: list[dict[str, str]] = []
|
||||
for index, item in enumerate(examples[:20], start=1):
|
||||
if not isinstance(item, dict):
|
||||
raise ValueError(f"第 {index} 个提示示例不是对象")
|
||||
example_input = _string(item.get("input"), 4000)
|
||||
example_output = _string(item.get("output"), 6000)
|
||||
if not example_input or not example_output:
|
||||
raise ValueError(f"第 {index} 个提示示例缺少 input 或 output")
|
||||
normalized.append({"input": example_input, "output": example_output})
|
||||
result["examples"] = normalized
|
||||
if "output_schema" in payload:
|
||||
schema = payload.get("output_schema")
|
||||
if isinstance(schema, str):
|
||||
try:
|
||||
schema = json.loads(schema)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError("输出 Schema 不是合法 JSON") from exc
|
||||
if not isinstance(schema, dict):
|
||||
raise ValueError("输出 Schema 必须是 JSON 对象")
|
||||
required = schema.get("required", [])
|
||||
if required and (not isinstance(required, list) or any(not isinstance(item, str) for item in required)):
|
||||
raise ValueError("输出 Schema 的 required 必须是字符串列表")
|
||||
result["output_schema"] = schema
|
||||
if "enabled" in payload:
|
||||
result["enabled"] = bool(payload.get("enabled"))
|
||||
return result
|
||||
|
||||
|
||||
def template_from_snapshot(snapshot: dict[str, Any] | None) -> dict[str, Any]:
|
||||
data = snapshot if isinstance(snapshot, dict) else {}
|
||||
return {
|
||||
"id": int(data.get("id") or 0),
|
||||
"name": _string(data.get("name") or "默认内容生成模板", 120),
|
||||
"role_prompt": _string(data.get("role_prompt") or DEFAULT_ROLE_PROMPT, 6000),
|
||||
"task_template": _string(data.get("task_template") or DEFAULT_TASK_TEMPLATE, 12000),
|
||||
"constraints": list(data.get("constraints") or DEFAULT_CONSTRAINTS),
|
||||
"examples": list(data.get("examples") or []),
|
||||
"output_schema": dict(data.get("output_schema") or DEFAULT_OUTPUT_SCHEMA),
|
||||
"style": _string(data.get("style") or "大白话、自然口语、适合直接播读", 6000),
|
||||
"version": int(data.get("version") or 1),
|
||||
}
|
||||
|
||||
|
||||
def render_prompt(template: dict[str, Any] | None, values: dict[str, object], *, rules: str = "",
|
||||
skills: str = "", memories: str = "", fusion: str = "", anchors: str = "",
|
||||
previous: str = "", knowledge: str = "", runtime_rules: str = "",
|
||||
next_context: str = "", qc_block: str = "", persona: str = "") -> str:
|
||||
"""按固定层次拼装提示,模板文本只替换已知变量,保留未知花括号。"""
|
||||
spec = template_from_snapshot(template)
|
||||
|
||||
def interpolate(text: str) -> str:
|
||||
rendered = text
|
||||
for key in PROMPT_PLACEHOLDERS:
|
||||
rendered = rendered.replace("{" + key + "}", _string(values.get(key)))
|
||||
rendered = rendered.replace("{{" + key + "}}", _string(values.get(key)))
|
||||
return rendered.strip()
|
||||
|
||||
sections = [
|
||||
("Agent 角色", interpolate(spec["role_prompt"])),
|
||||
("任务模板", interpolate(spec["task_template"])),
|
||||
]
|
||||
if spec["style"]:
|
||||
sections.append(("模板基础风格(可被 Agent 规则与人格档案覆盖)", interpolate(spec["style"])))
|
||||
if rules:
|
||||
sections.append(("Agent 规则", rules))
|
||||
if persona and persona.strip():
|
||||
sections.append(("人格档案", persona.strip()))
|
||||
if spec["constraints"]:
|
||||
sections.append(("模板约束", "\n".join(f"- {item}" for item in spec["constraints"])))
|
||||
if spec["examples"]:
|
||||
examples = ["## 示例(仅用于学习格式,不是事实来源)"]
|
||||
for index, item in enumerate(spec["examples"], start=1):
|
||||
examples.append(f"示例 {index} 输入:\n{item.get('input', '')}\n示例 {index} 输出:\n{item.get('output', '')}")
|
||||
sections.append(("上下文示例", "\n\n".join(examples)))
|
||||
for title, content in (("已挂接技能", skills), ("融合协作", fusion), ("已确认记忆", memories),
|
||||
("上游锚点", anchors), ("上一次任务衔接", previous),
|
||||
("知识库材料", knowledge), ("运行硬规则", runtime_rules),
|
||||
("相邻段约束", next_context), ("上次质检意见", qc_block)):
|
||||
if content and content.strip():
|
||||
sections.append((title, content.strip()))
|
||||
schema = json.dumps(spec["output_schema"], ensure_ascii=False, indent=2)
|
||||
sections.append(("输出协议", f"正文完成后输出锚点卡 JSON,必须符合以下 Schema:\n```json\n{schema}\n```"))
|
||||
return "\n\n".join(f"## {title}\n{content}" for title, content in sections if content)
|
||||
|
||||
|
||||
def validate_schema(value: object, schema: dict[str, Any] | None) -> tuple[bool, str]:
|
||||
"""提供轻量 JSON Schema 校验,重点覆盖 required/type,避免新增运行依赖。"""
|
||||
if not schema:
|
||||
return True, ""
|
||||
if schema.get("type") == "object" and not isinstance(value, dict):
|
||||
return False, "输出不是 JSON 对象"
|
||||
if isinstance(value, dict):
|
||||
required = schema.get("required") or []
|
||||
properties = schema.get("properties") or {}
|
||||
missing: list[str] = []
|
||||
for key in required:
|
||||
if key not in value:
|
||||
missing.append(key)
|
||||
continue
|
||||
rule = properties.get(key)
|
||||
actual = value[key]
|
||||
if isinstance(rule, dict) and rule.get("type") == "string" and not isinstance(actual, str):
|
||||
return False, f"字段 {key} 不是字符串"
|
||||
if actual is None or (isinstance(actual, str) and not actual.strip()):
|
||||
missing.append(key)
|
||||
if missing:
|
||||
return False, "缺少必填字段:" + "、".join(missing)
|
||||
for key, rule in properties.items():
|
||||
if key not in value or not isinstance(rule, dict) or "type" not in rule:
|
||||
continue
|
||||
expected = rule["type"]
|
||||
actual = value[key]
|
||||
if expected == "string" and not isinstance(actual, str):
|
||||
return False, f"字段 {key} 不是字符串"
|
||||
if expected == "array" and not isinstance(actual, list):
|
||||
return False, f"字段 {key} 不是数组"
|
||||
if expected == "object" and not isinstance(actual, dict):
|
||||
return False, f"字段 {key} 不是对象"
|
||||
return True, ""
|
||||
Reference in New Issue
Block a user