feat: 通用QC/PM门禁声明式豁免——params.skip_generic_qc任务交付直接approved(qc_review_run/pm_review_run兜底放行存量卡住任务)
This commit is contained in:
parent
9d40816fac
commit
ae78742479
@ -2766,6 +2766,23 @@ async def pm_review_run(project_id, agent_id=None, model_name=None):
|
||||
task_id = task.id
|
||||
title = getattr(task, "title", "") or ""
|
||||
task_role = _normalize_role(getattr(task, "role", "") or "")
|
||||
# 豁免兜底(2026-09-03):产线自带质量门禁的任务(如投标产线,
|
||||
# params.task_kind=bid_* / skip_generic_qc)PM 不复审内容,直接放行——
|
||||
# 质量判定归产线流转引擎(章节评审打分 + 解析产出契合度审核)。
|
||||
_skip_gqc = False
|
||||
try:
|
||||
_tpr = json.loads(getattr(task, "params", "") or "{}")
|
||||
if isinstance(_tpr, dict):
|
||||
_skip_gqc = bool(_tpr.get("skip_generic_qc")) or \
|
||||
str(_tpr.get("task_kind", "") or "").startswith("bid_")
|
||||
except Exception:
|
||||
pass
|
||||
if _skip_gqc:
|
||||
from .task_capability import approve_task
|
||||
await approve_task(task_id, project_id, who="agent.pm", agent_id=agent_id,
|
||||
comment="产线自带质量门禁,PM 豁免内容复审")
|
||||
logger.info(f"pm_review_run exempt: task={task_id} -> approved (pipeline-owned QC)")
|
||||
return {"status": "exempted", "task_id": task_id}
|
||||
# 机构 llm 检测:机构没配 llm → 冒泡问题暂停
|
||||
llm_missing, _names = await _check_org_llm(sor, org_id)
|
||||
if llm_missing:
|
||||
@ -3087,13 +3104,26 @@ async def qc_review_run(project_id, agent_id=None, model_name=None):
|
||||
# 判断是否 human_task_qc(人类任务 QC,检查对象是 pipeline_human_tasks 而非交付件)
|
||||
task_kind = ""
|
||||
human_task_id = ""
|
||||
skip_generic_qc = False
|
||||
try:
|
||||
_tp = json.loads(getattr(task, "params", "") or "{}")
|
||||
if isinstance(_tp, dict):
|
||||
task_kind = _tp.get("task_kind", "") or ""
|
||||
human_task_id = _tp.get("human_task_id", "") or ""
|
||||
skip_generic_qc = bool(_tp.get("skip_generic_qc"))
|
||||
except Exception:
|
||||
pass
|
||||
# 豁免兜底(2026-09-03):声明 skip_generic_qc 的产线任务自带质量门禁
|
||||
# (如投标产线的章节评审 + 契合度审核),通用门禁不接管;存量卡在此状态
|
||||
# 的任务直接放行,避免被当「交付件合规检查」反复拒回烧轮次。
|
||||
# task_kind=bid_* 兜底覆盖改造前创建的存量任务(params 里没有豁免标记)。
|
||||
if skip_generic_qc or task_kind.startswith("bid_"):
|
||||
from .task_capability import qc_exempt_task
|
||||
await qc_exempt_task(task_id, project_id, who="agent.qc", agent_id=agent_id,
|
||||
comment="产线自带质量门禁,豁免通用QC(skip_generic_qc/bid_*)")
|
||||
logger.info(f"qc_review_run exempt: task={task_id} -> approved "
|
||||
f"(skip_generic_qc={skip_generic_qc} task_kind={task_kind})")
|
||||
return {"status": "exempted", "task_id": task_id}
|
||||
# 机构 llm 检测:机构没配 llm → 冒泡问题暂停
|
||||
llm_missing, _names = await _check_org_llm(sor, org_id)
|
||||
if llm_missing:
|
||||
|
||||
@ -1,521 +0,0 @@
|
||||
"""
|
||||
pipeline-service: platform_ability — 平台内部 agent 能力包(pipeline_id=platform_general)
|
||||
|
||||
产线平台自身的运维/管理 agent:拥有平台应用和各模块的技能,
|
||||
核心能力是「通读模型 API 文档 → 自动生成模型治理配置(供应商/适配模板/模型/定价)」。
|
||||
|
||||
权限模型(代码层硬门禁,不依赖 prompt):
|
||||
所有工具 handler 入口校验调用者的 RBAC 角色 ∈ {owner.superuser, owner.admin}。
|
||||
内部 agent 只服务管理员——普通用户即使打开页面也调不动任何工具。
|
||||
|
||||
配置生成链路(用户给定 API 文档 → 完成配置):
|
||||
① fetch_model_doc:抓取文档页面(SSRF 防护:仅 http(s) 公网域名)
|
||||
② extract_llm_api_spec:LLM 从文档提取端点/协议/请求响应格式/定价
|
||||
③ apply_llm_config:写库——复用 pipeline-llm 模块的 CRUD 函数
|
||||
(llm_vendor.endpoints + llm_api_profile 模板 + llm_model 含四价)
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
|
||||
from pipeline_core import (
|
||||
ToolDefinition,
|
||||
PipelineAbility,
|
||||
register_ability,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("pipeline.platform_ability")
|
||||
|
||||
PLATFORM_PIPELINE_ID = "platform_general"
|
||||
|
||||
# 管理角色:内部 agent 仅管理员可用
|
||||
ADMIN_ROLES = ("owner.superuser", "owner.admin")
|
||||
|
||||
# 当前运行时调用链支持的协议(llm_bridge 固定 OpenAI 兼容路径)。
|
||||
# 其他协议的适配模板会存入 llm_api_profile 备查,但运行时暂不渲染。
|
||||
RUNTIME_PROTOCOLS = ("openai_compat",)
|
||||
|
||||
|
||||
# ────────────────────── 权限门禁(代码层) ──────────────────────
|
||||
|
||||
async def _require_admin(sor, ctx) -> str:
|
||||
"""校验当前用户是管理员。返回 ''=通过,否则错误信息。"""
|
||||
uid = ctx.get("user_id", "") or ""
|
||||
if not uid:
|
||||
return "无法识别当前用户身份(未登录),内部 agent 仅管理员可用"
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT r.orgtypeid, r.name FROM userrole ur JOIN role r ON ur.roleid=r.id "
|
||||
"WHERE ur.userid=${u}$", {"u": uid})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
roles = set()
|
||||
for r in (recs or []):
|
||||
o = getattr(r, "orgtypeid", "") or ""
|
||||
n = getattr(r, "name", "") or ""
|
||||
if o and n:
|
||||
roles.add("%s.%s" % (o, n))
|
||||
if roles & set(ADMIN_ROLES):
|
||||
return ""
|
||||
return "权限不足:内部 agent 工具仅管理员(%s)可用,当前角色 %s" % (
|
||||
"/".join(ADMIN_ROLES), sorted(roles) or "无")
|
||||
|
||||
|
||||
def _row(r):
|
||||
"""sqlor row → dict(容忍属性访问)。"""
|
||||
if hasattr(r, "__dict__"):
|
||||
return {k: v for k, v in r.__dict__.items() if not k.startswith("_")}
|
||||
return {}
|
||||
|
||||
|
||||
# ────────────────────── 工具 1:模型治理状态 ──────────────────────
|
||||
|
||||
async def _h_platform_llm_status(sor, params, ctx):
|
||||
"""供应商/账号/模型/用量概览(管理员诊断配置用)。"""
|
||||
err = await _require_admin(sor, ctx)
|
||||
if err:
|
||||
return err
|
||||
out = {}
|
||||
for tbl in ("llm_vendor", "llm_account", "llm_model"):
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT status, COUNT(*) AS c FROM " + tbl + " GROUP BY status", {})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
out[tbl] = {getattr(r, "status", ""): int(getattr(r, "c", 0)) for r in (recs or [])}
|
||||
accs = await sor.sqlExe(
|
||||
"SELECT name, balance, status FROM llm_account ORDER BY balance DESC LIMIT 20", {})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
out["accounts"] = [
|
||||
{"name": getattr(r, "name", ""), "balance": float(getattr(r, "balance", 0) or 0),
|
||||
"status": getattr(r, "status", "")} for r in (accs or [])]
|
||||
models = await sor.sqlExe(
|
||||
"SELECT name, vendor_model_id, capability, price_input, price_output, status "
|
||||
"FROM llm_model ORDER BY created_at DESC LIMIT 30", {})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
out["models"] = [
|
||||
{"name": getattr(r, "name", ""), "vendor_model_id": getattr(r, "vendor_model_id", ""),
|
||||
"capability": getattr(r, "capability", ""),
|
||||
"price_input": float(getattr(r, "price_input", 0) or 0),
|
||||
"price_output": float(getattr(r, "price_output", 0) or 0),
|
||||
"status": getattr(r, "status", "")} for r in (models or [])]
|
||||
return json.dumps(out, ensure_ascii=False)
|
||||
|
||||
|
||||
# ────────────────────── 工具 2:抓取模型 API 文档 ──────────────────────
|
||||
|
||||
_MAX_DOC_CHARS = 60000
|
||||
|
||||
|
||||
def _validate_doc_url(url: str) -> str:
|
||||
"""SSRF 防护:仅允许 http(s) + 公网域名。返回 ''=通过,否则错误。"""
|
||||
if not re.match(r"^https?://", url or ""):
|
||||
return "文档 URL 必须是 http/https 链接"
|
||||
host = re.sub(r"^https?://", "", url).split("/")[0].split(":")[0].lower()
|
||||
if not re.match(r"^[a-z0-9][a-z0-9.-]+\.[a-z]{2,}$", host):
|
||||
return "文档 URL 域名非法(不接受 IP/内网地址)"
|
||||
if host in ("localhost",) or host.endswith((".local", ".internal", ".localhost")):
|
||||
return "文档 URL 不允许指向内网/本地地址"
|
||||
return ""
|
||||
|
||||
|
||||
def _html_to_text(html: str) -> str:
|
||||
"""粗暴去标签提取文本(文档页面用,不追求完美排版)。"""
|
||||
txt = re.sub(r"(?is)<(script|style|noscript)[^>]*>.*?</\1>", " ", html or "")
|
||||
txt = re.sub(r"(?is)<[^>]+>", " ", txt)
|
||||
txt = re.sub(r" ", " ", txt)
|
||||
txt = re.sub(r"<", "<", txt)
|
||||
txt = re.sub(r">", ">", txt)
|
||||
txt = re.sub(r"&", "&", txt)
|
||||
txt = re.sub(r""", '"', txt)
|
||||
txt = re.sub(r"[ \t]+", " ", txt)
|
||||
txt = re.sub(r"\n\s*\n+", "\n", txt)
|
||||
return txt.strip()
|
||||
|
||||
|
||||
async def _h_fetch_model_doc(sor, params, ctx):
|
||||
"""抓取模型 API 文档页面 → 纯文本(供 LLM 提取配置规格)。"""
|
||||
err = await _require_admin(sor, ctx)
|
||||
if err:
|
||||
return err
|
||||
url = (params.get("url") or "").strip()
|
||||
verr = _validate_doc_url(url)
|
||||
if verr:
|
||||
return verr
|
||||
import aiohttp
|
||||
try:
|
||||
timeout = aiohttp.ClientTimeout(total=30)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as sess:
|
||||
async with sess.get(url, headers={"User-Agent": "Mozilla/5.0"},
|
||||
allow_redirects=True, ssl=False) as resp:
|
||||
if resp.status != 200:
|
||||
return "抓取失败:HTTP %d" % resp.status
|
||||
ctype = resp.headers.get("Content-Type", "")
|
||||
raw = await resp.text(errors="replace")
|
||||
except Exception as e:
|
||||
return "抓取失败:%s" % str(e)[:200]
|
||||
if "html" in ctype.lower() or raw.lstrip()[:15].lower().startswith(("<!doctype", "<html")):
|
||||
text = _html_to_text(raw)
|
||||
else:
|
||||
text = raw
|
||||
if len(text) > _MAX_DOC_CHARS:
|
||||
text = text[:_MAX_DOC_CHARS] + "\n[文档过长已截断,共 %d 字符]" % len(text)
|
||||
return text
|
||||
|
||||
|
||||
# ────────────────────── 工具 3:提取配置规格(LLM) ──────────────────────
|
||||
|
||||
_EXTRACT_PROMPT = """你是大模型 API 配置专家。通读下面这份模型 API 文档,提取配置规格。
|
||||
|
||||
只输出一个 JSON 对象(不要 markdown 代码块),字段:
|
||||
{
|
||||
"vendor_name": "供应商名称",
|
||||
"base_url": "API 基础地址(如 https://dashscope.aliyuncs.com/compatible-mode/v1)",
|
||||
"protocol": "openai_compat | dashscope_async | custom(能走 /chat/completions 的填 openai_compat)",
|
||||
"endpoints": [{"base_url": "...", "region": "domestic|international", "timeout": 60}],
|
||||
"chat_path": "对话接口路径(如 /chat/completions)",
|
||||
"auth_header": "认证头格式说明(如 Bearer API_KEY)",
|
||||
"request_fields": ["请求体字段名列表"],
|
||||
"response_format": "响应格式说明(content 字段路径 + usage 字段路径)",
|
||||
"models": [{"vendor_model_id": "供应商侧模型ID", "capability": "t2t|t2i|i2t|t2v|embedding|rerank|tts|asr",
|
||||
"price_input": 输入价元/千token或null, "price_output": 输出价元/千token或null,
|
||||
"cost_input": 输入成本元/千token或null, "cost_output": 输出成本元/千token或null,
|
||||
"description": "一句话说明"}],
|
||||
"doc_notes": "文档中影响配置的关键注意点"
|
||||
}
|
||||
|
||||
定价规则:
|
||||
- 文档给的单位若是「元/百万tokens」,换算为「元/千tokens」= 原价/1000,保留6位小数
|
||||
- 文档没写价格的模型:价格字段填 null(禁止编造价格),在 description 注明
|
||||
- cost_* 未知时与 price_* 相同(无供应商折扣时成本=售价)
|
||||
|
||||
文档内容:
|
||||
"""
|
||||
|
||||
|
||||
async def _h_extract_llm_api_spec(sor, params, ctx):
|
||||
"""LLM 通读文档文本 → 结构化配置规格(JSON)。"""
|
||||
err = await _require_admin(sor, ctx)
|
||||
if err:
|
||||
return err
|
||||
doc_text = (params.get("doc_text") or "").strip()
|
||||
if len(doc_text) < 100:
|
||||
return "文档文本太短(<100字符),无法提取配置——先用 fetch_model_doc 抓取"
|
||||
try:
|
||||
from .llm_bridge import llm_call_msgs
|
||||
raw = await llm_call_msgs(
|
||||
[{"role": "system", "content": _EXTRACT_PROMPT},
|
||||
{"role": "user", "content": doc_text[:48000]}],
|
||||
temperature=0.1, org_id="0", user_id=ctx.get("user_id", ""))
|
||||
except Exception as e:
|
||||
return "LLM 提取失败:%s" % str(e)[:200]
|
||||
txt = (raw or "").strip()
|
||||
if txt.startswith("```"):
|
||||
txt = txt.split("\n", 1)[1] if "\n" in txt else txt
|
||||
txt = txt.rsplit("```", 1)[0]
|
||||
try:
|
||||
spec = json.loads(txt)
|
||||
except Exception:
|
||||
m = re.search(r"\{.*\}", txt, re.S)
|
||||
if not m:
|
||||
return "LLM 输出不是合法 JSON:%s" % txt[:300]
|
||||
try:
|
||||
spec = json.loads(m.group(0))
|
||||
except Exception:
|
||||
return "LLM 输出不是合法 JSON:%s" % txt[:300]
|
||||
return json.dumps(spec, ensure_ascii=False)
|
||||
|
||||
|
||||
# ────────────────────── 工具 4:写入配置(幂等) ──────────────────────
|
||||
|
||||
def _f(v, default=0.0):
|
||||
try:
|
||||
return float(v)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
async def _h_apply_llm_config(sor, params, ctx):
|
||||
"""按提取的规格写库:供应商(复用/新建)+ 适配模板 + 模型(含定价)。
|
||||
|
||||
幂等:供应商按名称复用;模型按 (vendor, vendor_model_id) 复用——
|
||||
已存在则更新定价,不重复创建。
|
||||
"""
|
||||
err = await _require_admin(sor, ctx)
|
||||
if err:
|
||||
return err
|
||||
try:
|
||||
spec = json.loads(params.get("spec") or "{}")
|
||||
except Exception:
|
||||
return "spec 不是合法 JSON"
|
||||
vendor_name = (spec.get("vendor_name") or "").strip()
|
||||
if not vendor_name:
|
||||
return "spec 缺少 vendor_name"
|
||||
endpoints = spec.get("endpoints") or []
|
||||
if not endpoints and spec.get("base_url"):
|
||||
endpoints = [{"base_url": spec["base_url"], "region": "domestic", "timeout": 60}]
|
||||
if not endpoints:
|
||||
return "spec 缺少 endpoints/base_url"
|
||||
protocol = (spec.get("protocol") or "openai_compat").strip() or "openai_compat"
|
||||
models = spec.get("models") or []
|
||||
if not models:
|
||||
return "spec.models 为空——文档里没有可配置的模型?"
|
||||
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
# 1. 供应商:按名称复用
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT id, endpoints FROM llm_vendor WHERE name=${n}$", {"n": vendor_name})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
if recs:
|
||||
vendor_id = getattr(recs[0], "id", "")
|
||||
eps_old = []
|
||||
try:
|
||||
eps_old = json.loads(getattr(recs[0], "endpoints", "") or "[]")
|
||||
except Exception:
|
||||
eps_old = []
|
||||
merged = list(eps_old)
|
||||
added_eps = []
|
||||
for ep in endpoints:
|
||||
bu = (ep.get("base_url") or "").rstrip("/")
|
||||
if bu and bu not in [(e.get("base_url") or "").rstrip("/") for e in merged]:
|
||||
merged.append({"base_url": bu, "region": ep.get("region") or "domestic",
|
||||
"timeout": int(_f(ep.get("timeout"), 60))})
|
||||
added_eps.append(bu)
|
||||
await sor.sqlExe(
|
||||
"UPDATE llm_vendor SET endpoints=${e}$, protocol=${p}$, updated_at=NOW() "
|
||||
"WHERE id=${i}$",
|
||||
{"e": json.dumps(merged, ensure_ascii=False), "p": protocol, "i": vendor_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
vendor_action = "复用供应商 %s(新增端点 %d 个)" % (vendor_id, len(added_eps))
|
||||
else:
|
||||
vendor_id = getID()
|
||||
eps_norm = [{"base_url": (ep.get("base_url") or "").rstrip("/"),
|
||||
"region": ep.get("region") or "domestic",
|
||||
"timeout": int(_f(ep.get("timeout"), 60))} for ep in endpoints]
|
||||
await sor.C("llm_vendor", {
|
||||
"id": vendor_id, "name": vendor_name, "protocol": protocol,
|
||||
"endpoints": json.dumps(eps_norm, ensure_ascii=False),
|
||||
"description": spec.get("doc_notes", "") or "",
|
||||
"status": "active", "org_id": "0"})
|
||||
vendor_action = "新建供应商 %s(%s)" % (vendor_id, vendor_name)
|
||||
|
||||
# 2. 适配模板:按 (协议×能力) 复用,缺则新建
|
||||
profile_ids = {}
|
||||
caps = sorted(set((m.get("capability") or "t2t") for m in models))
|
||||
for cap in caps:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT id FROM llm_api_profile WHERE protocol=${p}$ AND capability=${c}$ "
|
||||
"AND status='active' ORDER BY created_at DESC LIMIT 1",
|
||||
{"p": protocol, "c": cap})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
if recs:
|
||||
profile_ids[cap] = getattr(recs[0], "id", "")
|
||||
continue
|
||||
req_tpl, resp_tpl = _default_templates(protocol, cap, spec)
|
||||
pid = getID()
|
||||
await sor.C("llm_api_profile", {
|
||||
"id": pid, "name": "%s-%s-自动配置" % (protocol, cap),
|
||||
"protocol": protocol, "capability": cap,
|
||||
"request_template": req_tpl, "response_template": resp_tpl,
|
||||
"param_schema": json.dumps([{"name": "prompt", "label": "提示词",
|
||||
"uitype": "textarea", "required": True}],
|
||||
ensure_ascii=False),
|
||||
"status": "active"})
|
||||
profile_ids[cap] = pid
|
||||
|
||||
# 3. 模型:按 (vendor, vendor_model_id) 幂等
|
||||
created, updated, skipped = [], [], []
|
||||
for m in models:
|
||||
vmid = (m.get("vendor_model_id") or "").strip()
|
||||
if not vmid:
|
||||
skipped.append("(缺 vendor_model_id)")
|
||||
continue
|
||||
cap = m.get("capability") or "t2t"
|
||||
price_in, price_out = _f(m.get("price_input")), _f(m.get("price_output"))
|
||||
cost_in = _f(m.get("cost_input"), price_in)
|
||||
cost_out = _f(m.get("cost_output"), price_out)
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT id FROM llm_model WHERE vendor_id=${v}$ AND vendor_model_id=${m}$",
|
||||
{"v": vendor_id, "m": vmid})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
if recs:
|
||||
mid = getattr(recs[0], "id", "")
|
||||
await sor.sqlExe(
|
||||
"UPDATE llm_model SET price_input=${pi}$, price_output=${po}$, "
|
||||
"cost_input=${ci}$, cost_output=${co}$, description=${d}$, updated_at=NOW() "
|
||||
"WHERE id=${i}$",
|
||||
{"pi": price_in, "po": price_out, "ci": cost_in, "co": cost_out,
|
||||
"d": m.get("description", "") or "", "i": mid})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
updated.append(vmid)
|
||||
continue
|
||||
mid = getID()
|
||||
await sor.C("llm_model", {
|
||||
"id": mid, "vendor_id": vendor_id, "account_id": "",
|
||||
"name": vmid, "vendor_model_id": vmid, "capability": cap,
|
||||
"sync_mode": "sync", "profile_id": profile_ids.get(cap, ""),
|
||||
"ppid": "", "price_input": price_in, "price_output": price_out,
|
||||
"cost_input": cost_in, "cost_output": cost_out,
|
||||
"default_params": "{}", "status": "active",
|
||||
"description": m.get("description", "") or "", "org_id": "0"})
|
||||
created.append(vmid)
|
||||
|
||||
rt_note = ""
|
||||
if protocol not in RUNTIME_PROTOCOLS:
|
||||
rt_note = ("⚠️ 协议「%s」的适配模板已存档,但当前运行时调用链仅支持 %s——"
|
||||
"该供应商模型暂不可被产线直接调用" % (protocol, "/".join(RUNTIME_PROTOCOLS)))
|
||||
return json.dumps({
|
||||
"vendor": vendor_action, "profiles": profile_ids,
|
||||
"models_created": created, "models_updated": updated, "models_skipped": skipped,
|
||||
"runtime_note": rt_note,
|
||||
}, ensure_ascii=False)
|
||||
|
||||
|
||||
def _default_templates(protocol: str, capability: str, spec: dict) -> tuple:
|
||||
"""生成适配模板默认值(headers/data/response)。
|
||||
|
||||
OpenAI 兼容协议用平台标准模板;其他协议按文档格式生成骨架供人工微调。
|
||||
"""
|
||||
chat_path = (spec.get("chat_path") or "/chat/completions").strip()
|
||||
if protocol == "openai_compat":
|
||||
req = json.dumps({
|
||||
"path": chat_path, "method": "POST",
|
||||
"headers": {"Authorization": "Bearer {{api_key}}",
|
||||
"Content-Type": "application/json"},
|
||||
"params": {},
|
||||
"data": {"model": "{{model}}", "messages": "{{messages}}",
|
||||
"temperature": "{{temperature}}", "stream": False},
|
||||
}, ensure_ascii=False)
|
||||
resp = json.dumps({
|
||||
"content": "choices[0].message.content",
|
||||
"usage": {"prompt_tokens": "usage.prompt_tokens",
|
||||
"completion_tokens": "usage.completion_tokens"},
|
||||
}, ensure_ascii=False)
|
||||
return req, resp
|
||||
# 非标准协议:骨架模板(字段路径来自文档提取),标注需人工核对
|
||||
req = json.dumps({
|
||||
"path": chat_path, "method": "POST",
|
||||
"headers": {"Authorization": "Bearer {{api_key}}",
|
||||
"Content-Type": "application/json"},
|
||||
"params": {},
|
||||
"data": {"__from_doc__": spec.get("request_fields") or []},
|
||||
"__note__": "非 openai_compat 协议骨架模板,需按文档人工核对",
|
||||
}, ensure_ascii=False)
|
||||
resp = json.dumps({
|
||||
"content": spec.get("response_format", "") or "待按文档填写",
|
||||
"__note__": "需人工核对",
|
||||
}, ensure_ascii=False)
|
||||
return req, resp
|
||||
|
||||
|
||||
# ────────────────────── 工具 5:模块/应用信息查询 ──────────────────────
|
||||
|
||||
async def _h_platform_modules(sor, params, ctx):
|
||||
"""列出平台已装载的业务模块(内部 agent 了解平台构成用)。"""
|
||||
err = await _require_admin(sor, ctx)
|
||||
if err:
|
||||
return err
|
||||
mods = []
|
||||
for name, title in [
|
||||
("pipeline_core", "产线核心(会话/技能/项目)"),
|
||||
("pipeline_service", "执行引擎(任务链/角色/治理)"),
|
||||
("pipeline-llm", "模型治理(供应商/账号/模型/定价/记账)"),
|
||||
("pipeline-bidding", "投标产线"),
|
||||
("pipeline-opportunity", "商机产线"),
|
||||
("pipeline-sdlc", "开发产线前端"),
|
||||
("pipeline-ops", "运维"),
|
||||
("pipeline-dist", "分销"),
|
||||
("pipeline-task", "任务中心"),
|
||||
]:
|
||||
try:
|
||||
__import__(name.replace("-", "_"))
|
||||
loaded = True
|
||||
except Exception:
|
||||
loaded = False
|
||||
mods.append({"module": name, "title": title, "loaded": loaded})
|
||||
return json.dumps(mods, ensure_ascii=False)
|
||||
|
||||
|
||||
# ────────────────────── 工具定义 ──────────────────────
|
||||
|
||||
PLATFORM_TOOLS = [
|
||||
ToolDefinition(
|
||||
name="platform_llm_status",
|
||||
description="查看模型治理状态:供应商/账号余额/模型定价/各状态分布。用户问「模型配置状态/账号余额」时调用。仅管理员可用。",
|
||||
parameters={},
|
||||
category="platform",
|
||||
),
|
||||
ToolDefinition(
|
||||
name="fetch_model_doc",
|
||||
description="抓取大模型供应商的 API 文档页面(返回纯文本)。配置新模型前先用此抓取官方文档。仅管理员可用。",
|
||||
parameters={"url": "文档页面 URL(必须是公网 http/https)"},
|
||||
category="platform",
|
||||
),
|
||||
ToolDefinition(
|
||||
name="extract_llm_api_spec",
|
||||
description="通读文档文本,LLM 提取 API 配置规格(端点/协议/请求响应格式/定价)。配合 fetch_model_doc 使用。仅管理员可用。",
|
||||
parameters={"doc_text": "fetch_model_doc 返回的文档文本"},
|
||||
category="platform",
|
||||
),
|
||||
ToolDefinition(
|
||||
name="apply_llm_config",
|
||||
description="按提取的规格写入模型治理配置:供应商/端点/适配模板/模型(含定价)。幂等——已有模型只更新定价。仅管理员可用。",
|
||||
parameters={"spec": "extract_llm_api_spec 返回的 JSON 规格"},
|
||||
category="platform",
|
||||
requires_confirmation=True,
|
||||
),
|
||||
ToolDefinition(
|
||||
name="platform_modules",
|
||||
description="列出平台已装载的业务模块清单。用户问「平台有哪些模块」时调用。仅管理员可用。",
|
||||
parameters={},
|
||||
category="platform",
|
||||
),
|
||||
]
|
||||
|
||||
PLATFORM_PROMPT = """
|
||||
你是产线平台的内部运维 agent,服务对象是平台管理员。
|
||||
|
||||
## 模型自动配置工作流(用户给你文档 URL 要求配置模型时)
|
||||
1. `fetch_model_doc` 抓取官方文档页面
|
||||
2. `extract_llm_api_spec` 通读文档提取配置规格——**定价只允许来自文档原文**,文档没写的价格一律 null,禁止编造
|
||||
3. 把提取结果摘要给用户确认(尤其定价和端点),用户确认后
|
||||
4. `apply_llm_config` 写入(幂等:已有模型只更新定价)
|
||||
|
||||
## 硬规则
|
||||
- 定价单位统一「元/千tokens」(文档给百万价要 ÷1000)
|
||||
- cost_*(供应商成本)未知时与 price_* 相同
|
||||
- 非 openai_compat 协议的模型:模板会存档但运行时暂不可调用,必须如实告知
|
||||
- 文档抓取失败/内容不足时如实说明,不要凭记忆编造 API 格式
|
||||
- 所有工具仅管理员可用,权限报错时如实转告用户
|
||||
|
||||
## 平台知识
|
||||
平台模块清单用 `platform_modules` 查询;技能库中「平台」相关技能(skills_library/all/ 与
|
||||
pipelines/platform_general/)包含各模块的开发规范与踩坑记录,需要时用 load_skill 加载。
|
||||
"""
|
||||
|
||||
PLATFORM_HANDLERS = {
|
||||
"platform_llm_status": _h_platform_llm_status,
|
||||
"fetch_model_doc": _h_fetch_model_doc,
|
||||
"extract_llm_api_spec": _h_extract_llm_api_spec,
|
||||
"apply_llm_config": _h_apply_llm_config,
|
||||
"platform_modules": _h_platform_modules,
|
||||
}
|
||||
|
||||
|
||||
def register_platform_ability():
|
||||
"""注册平台内部 agent 能力包(幂等)。"""
|
||||
ability = PipelineAbility(
|
||||
pipeline_id=PLATFORM_PIPELINE_ID,
|
||||
name="平台内部 agent",
|
||||
tools=PLATFORM_TOOLS,
|
||||
system_prompt=PLATFORM_PROMPT,
|
||||
handlers=PLATFORM_HANDLERS,
|
||||
roles=[],
|
||||
menus=[
|
||||
{"label": "📊 模型治理", "icon": "", "url": "/pipeline-llm",
|
||||
"type": "tab"},
|
||||
],
|
||||
)
|
||||
register_ability(ability)
|
||||
return ability
|
||||
|
||||
|
||||
register_platform_ability()
|
||||
@ -8,6 +8,7 @@
|
||||
角色规范:role 参数用 agent.{role}(无前缀自动补 agent.);人角色用 {orgtype}.{role}。
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from sqlor.dbpools import DBPools
|
||||
from appPublic.uniqueID import getID
|
||||
@ -105,8 +106,35 @@ async def claim_task(tenant_id, role, agent_id, from_state=S_SUBMITTED,
|
||||
return True, task_id
|
||||
|
||||
|
||||
async def _task_skip_generic_qc(task_id):
|
||||
"""任务是否声明豁免通用 QC 门禁(params.skip_generic_qc,产线自带质量门禁时用)。"""
|
||||
if not task_id:
|
||||
return False
|
||||
db, dbname = _get_db()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT params FROM pipeline_tasks WHERE id=${tid}$", {"tid": task_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
if not recs:
|
||||
return False
|
||||
try:
|
||||
p = json.loads(getattr(recs[0], 'params', '') or '{}')
|
||||
except Exception:
|
||||
return False
|
||||
return isinstance(p, dict) and bool(p.get('skip_generic_qc'))
|
||||
|
||||
|
||||
async def submit_task(task_id, tenant_id, who=None, agent_id=None):
|
||||
"""提交产出:running → qc_review(清 claimed_by,先过 QC 合规/质量门禁)。"""
|
||||
"""提交产出:running → qc_review(清 claimed_by,先过 QC 合规/质量门禁)。
|
||||
|
||||
声明式豁免(2026-09-03):params.skip_generic_qc=true 的任务直接 running → approved,
|
||||
跳过通用 QC/PM 门禁——该任务的质量判定由产线自身流转引擎负责
|
||||
(如投标产线:章节评审打分 + 解析产出契合度审核),通用门禁不接管。
|
||||
"""
|
||||
if await _task_skip_generic_qc(task_id):
|
||||
return await _transition(task_id, tenant_id, S_RUNNING, S_APPROVED, 'submit',
|
||||
who=who, agent_id=agent_id,
|
||||
detail='skip_generic_qc:产线自带质量门禁,豁免通用QC/PM门禁')
|
||||
return await _transition(task_id, tenant_id, S_RUNNING, S_QC_REVIEW, 'submit',
|
||||
who=who, agent_id=agent_id)
|
||||
|
||||
@ -163,6 +191,15 @@ async def qc_reject_task(task_id, tenant_id, who=None, agent_id=None, comment=No
|
||||
who=who, agent_id=agent_id, comment=comment)
|
||||
|
||||
|
||||
async def qc_exempt_task(task_id, tenant_id, who=None, agent_id=None, comment=None):
|
||||
"""豁免放行:qc_review → approved(任务声明 skip_generic_qc 时,通用门禁的兜底放行)。
|
||||
|
||||
用于存量已卡在 qc_review 的豁免任务:认领后不再做交付件检查,直接放行。
|
||||
"""
|
||||
return await _transition(task_id, tenant_id, S_QC_REVIEW, S_APPROVED, 'qc_exempt',
|
||||
who=who, agent_id=agent_id, detail=comment)
|
||||
|
||||
|
||||
async def approve_task(task_id, tenant_id, who=None, agent_id=None, comment=None):
|
||||
"""审核通过:review → approved。"""
|
||||
return await _transition(task_id, tenant_id, S_REVIEW, S_APPROVED, 'approve',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user