From 25e71b548be743271fc112525bab04534d3c36fe Mon Sep 17 00:00:00 2001 From: yumoqing Date: Wed, 2 Sep 2026 15:26:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B9=B3=E5=8F=B0=E5=86=85=E9=83=A8age?= =?UTF-8?q?nt=E7=8B=AC=E7=AB=8B=E6=A8=A1=E5=9D=97(=E4=BB=8Epipeline-servic?= =?UTF-8?q?e=E8=BF=81=E5=87=BA)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 能力包(5工具/管理员硬门禁/模型自动配置)+前端入口+宿主集成说明 --- README.md | 28 + pipeline_platform/__init__.py | 14 + .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 790 bytes .../__pycache__/init.cpython-310.pyc | Bin 0 -> 975 bytes .../platform_ability.cpython-310.pyc | Bin 0 -> 19073 bytes pipeline_platform/init.py | 27 + pipeline_platform/platform_ability.py | 521 ++++++++++++++++++ pyproject.toml | 9 + wwwroot/agent_platform/index.ui | 65 +++ 9 files changed, 664 insertions(+) create mode 100644 pipeline_platform/__init__.py create mode 100644 pipeline_platform/__pycache__/__init__.cpython-310.pyc create mode 100644 pipeline_platform/__pycache__/init.cpython-310.pyc create mode 100644 pipeline_platform/__pycache__/platform_ability.cpython-310.pyc create mode 100644 pipeline_platform/init.py create mode 100644 pipeline_platform/platform_ability.py create mode 100644 pyproject.toml create mode 100644 wwwroot/agent_platform/index.ui diff --git a/README.md b/README.md index a40eb51..aae6db0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,30 @@ # pipeline-platform +产线平台内部 agent 模块(宿主不装则零接触)。 + +## 内容 + +- **能力包** `pipeline_platform/platform_ability.py`:pipeline_id=`platform_general` + - platform_llm_status — 模型治理状态(供应商/账号余额/模型定价) + - fetch_model_doc — 抓取模型 API 文档(SSRF 防护:仅公网域名) + - extract_llm_api_spec — LLM 通读文档提取配置规格(端点/协议/定价) + - apply_llm_config — 写入配置(幂等:供应商按名复用、模型按 (vendor, vendor_model_id) 只更新定价) + - platform_modules — 平台模块清单 +- **前端入口** `wwwroot/agent_platform/index.ui`:主菜单「平台内部助手」(仅管理员可见) +- **技能**随 pipeline-core `skills_library/pipelines/platform_general/` 分发(平台架构地图 + 模型自动配置工作流) + +## 权限 + +所有工具仅 `owner.superuser` / `owner.admin` 可用,**代码层硬门禁**(每个 handler 入口校验 +RBAC 角色),不依赖 prompt。菜单入口同样 Jinja `{% if is_admin %}` 门控。 + +## 定价真实性规则 + +- 定价只允许来自文档原文,文档没写一律 null(禁止编造) +- 单位统一「元/千tokens」(元/百万 ÷1000) +- 非 openai_compat 协议的模板存档但运行时不可调,如实告知 + +## 宿主集成(已接入) + +- `pipeline-app/build.sh`:clone/pip install/wwwroot 软链列表已含本模块 +- `pipeline-app/app/pipeline_app.py`:`load_pipeline_platform()`(在 load_pipeline_service 之后) diff --git a/pipeline_platform/__init__.py b/pipeline_platform/__init__.py new file mode 100644 index 0000000..2fd9ef0 --- /dev/null +++ b/pipeline_platform/__init__.py @@ -0,0 +1,14 @@ +"""pipeline_platform — 产线平台内部 agent 模块(宿主不装则零接触)。 + +内部 agent = 平台自身的运维/管理会话能力: + - 能力包(platform_ability):模型治理状态/模型自动配置(通读 API 文档→ + 供应商端点/适配模板/模型目录/定价)/平台模块清单 + - 前端入口(wwwroot/agent_platform):pipeline_id=platform_general 会话页 + - 技能随 pipeline-core skills_library/pipelines/platform_general 分发 + +权限模型:所有工具仅管理员(owner.superuser / owner.admin)可用,代码层硬门禁。 +""" + +__version__ = "0.1.0" + +PIPELINE_ID = "platform_general" diff --git a/pipeline_platform/__pycache__/__init__.cpython-310.pyc b/pipeline_platform/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9b4b4728e8823ad19c95249bb2677cf020c380bd GIT binary patch literal 790 zcmZWnO-~v@7+z6ZgE8?BG?}XhS$nTdo74kIV@P`AGC_CSY`d~#7fS4Dfi40S!3L^A zX-bGnYXkihWwHE&{s4};v$K2R-cw)LLi#buyvaMe&&T`BNON;;2G;(My~jH)hPfIv zf368=?!Z>&p>T|!;U@eOoXhXxCLb^hQ;}5wg@h0<3PB!+#6b9=ByXTgwSbKFF>QCr z`4^J=NRpbJsH4CmJ{U%3y-F&3{Z5KBy2hDqoMo+=Mp6fM`PAGww2qJZo%EF&cf0>W zEhC4@+InxbPiX16)y-4=D@z+ynok;?lGUtOi3^e`_d6vwLW{`h$hHPjBWF0UCWu0K z6C~ibfs;2c%+ES7(9Ef+##rYFL`bG?Ygu~H0JNSqyvTgDWmapK$(JC3knz1t z+67Wf(vv2QpR=|a2M}x|)qV4i6tTO7J2=`4^C?U3YZ zAQ_EDWl0LN!^p3y10r&gu&dvOtY$`q80=CGjX;a*lFTFJi6Dv! z7KJrAAaAmxj|%%AA4w%iZrANLD+&7`@5D2RF0Goc)1-Asw3eZ3&MlDLeXuS?!HGwS zgm^ik@G@eNa}wY-gdj{na!p#O_dDB$UZZbglky=I>x^Rl`GevOfgflGp=8>=!E0| literal 0 HcmV?d00001 diff --git a/pipeline_platform/__pycache__/init.cpython-310.pyc b/pipeline_platform/__pycache__/init.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dabe467a86fa2380e803903ace93b8b07558cd57 GIT binary patch literal 975 zcmaJ0F0bPu{%by1xE7_3lCZy|ozw#Ru=l%ggtD@B96+dU~P=*3Ta+ zk4Jfg4%x@a1$XEsZ1+3_g5ro^!WlS@HN3$QzJTLA5mbSMNO%UtLxhVVSxog~glyf` z4BgZ&+lK0mTTW7{l?wFLN@FwEI5?oWB3;@%oSKeC>Grp3d7pk=t}mBbmAPY{YTK>K zJY6qIx~Y2?WvN=;YV2eIB^j1VlmMs%4*-NN?$+|tpx>&jMjP1!y0Av`x!P`t&gPkR z$BLpp)D7L6XjSrh(zYD$h}c2RGgr0s4`8&m{;_SkzB<#fxt*uEQgiJ~?Rl~B?k#9m z%ZpM+1mN%*oqJi`FILM7445s!f4cOdzWf@JVaiP?BpS(_4(tyGriRDZpI~utV6C|Y z+kFJVLykaj3}yNV6yCr|ZW3YS@f!jW3K&CUR5S`Ph6Ln=Caz*M3AdRufC!V2CqBhl zOu|IWaRaEpVZh!)Bs__qVG@aTPHCJ*a#!jCJl8HG-P@&0J9IV&iAZWfGd;+d?oHRX z=G*DqZ9=P&jQzeKji9!fYxjc`k2~~0>J(mbw1n<@nxl~+>6i)nnxh)2OCb3_a=V}O z&8Noa;*fOC9cg|C#;_dmMTL!qMttP3$@RmAl}KogFX-mD<#*kFGN##{ZkfKARvl9} z6F%>Hj?A^uSbLA09^m~N_{;hgHiU!rgn#zm!*>d3Za)Np7chsraTE(UjC*lE?8p@N z+hGVf3>Fn)jVX#RGH=X6S3>jd9Qkxaz&Ifm82ZS|iO>soe&l-6A}K?=!3yuP5fM;C KjB*0k&Hn*rL}Cnw~fOK9vp@q;DMj%_Fszib) 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 literal 0 HcmV?d00001 diff --git a/pipeline_platform/init.py b/pipeline_platform/init.py new file mode 100644 index 0000000..3f08949 --- /dev/null +++ b/pipeline_platform/init.py @@ -0,0 +1,27 @@ +"""pipeline-platform 模块装载入口。 + +宿主应用(pipeline-app)在 init() 中调用 load_pipeline_platform() 即挂载: +能力包注册(platform_ability,import 即注册)。 +未装本模块的宿主完全零接触(不 import 本包则以上全部不发生)。 +""" + +import logging + +logger = logging.getLogger("pipeline.platform") + +_loaded = False + + +def load_pipeline_platform(): + """挂载平台内部 agent(幂等)。""" + global _loaded + if _loaded: + return True + try: + from . import platform_ability # noqa: F401(import 即注册能力包) + logger.info("[pipeline_platform] ability registered: platform_general") + _loaded = True + return True + except Exception as e: + logger.warning("[pipeline_platform] ability 注册失败: %s", str(e)[:200]) + return False diff --git a/pipeline_platform/platform_ability.py b/pipeline_platform/platform_ability.py new file mode 100644 index 0000000..432509d --- /dev/null +++ b/pipeline_platform/platform_ability.py @@ -0,0 +1,521 @@ +""" +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)[^>]*>.*?", " ", 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((" _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 pipeline_service.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() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..200af04 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "pipeline_platform" +version = "0.1.0" +description = "平台内部 agent:平台运维/管理会话能力(模型自动配置等),仅管理员可用" +dependencies = [] + +[tool.setuptools.packages.find] +where = ["."] +include = ["pipeline_platform*"] diff --git a/wwwroot/agent_platform/index.ui b/wwwroot/agent_platform/index.ui new file mode 100644 index 0000000..fb837d5 --- /dev/null +++ b/wwwroot/agent_platform/index.ui @@ -0,0 +1,65 @@ +{ + "widgettype": "VBox", + "options": {"width": "100%", "height": "100%", "padding": "0"}, + "subwidgets": [ + { + "widgettype": "HBox", + "options": {"width": "100%", "alignItems": "center", "padding": "16px 24px 8px 24px", "cheight": 6, "gap": "12px"}, + "subwidgets": [ + {"widgettype": "Title2", "options": {"text": "平台内部助手"}}, + {"widgettype": "Text", "options": {"text": "平台运维/管理 agent · 模型自动配置 · 仅管理员可用", "cfontsize": 0.9, "color": "#94a3b8"}}, + {"widgettype": "Filler"}, + { + "widgettype": "Button", + "id": "new_session_btn", + "options": {"label": "+ 新建会话", "css": "small"}, + "binds": [{ + "wid": "self", + "event": "click", + "actiontype": "script", + "target": "self", + "script": "var tp=bricks.getWidgetById('platform_tabs',bricks.app);if(!tp)return;var sid='p'+Date.now()+'_'+Math.floor(Math.random()*100000);var n=tp.opts.items.length+1;tp.open_tab({name:'psession_'+sid,label:'会话 '+n,removable:true,content:{widgettype:'VBox',options:{css:'filler',width:'100%',height:'100%'},subwidgets:[{widgettype:'urlwidget',options:{url:'/pipeline_core/api/agent_menus.dspy?session_id='+sid+'&pipeline_id=platform_general',method:'GET'}},{widgettype:'AgentIO',options:{css:'filler',margin:'0 24px 24px 24px',url:'/pipeline_core/api/agent_chat.dspy?session_id='+sid+'&pipeline_id=platform_general',model_dataurl:'/pipeline_core/api/agent_model_options.dspy?session_id='+sid+'&pipeline_id=platform_general',model_cwidth:14,placeholder:'例如:通读这份文档并配置模型 <文档URL>...'}}]}});" + }] + } + ] + }, + { + "widgettype": "TabPanel", + "id": "platform_tabs", + "options": { + "css": "filler", + "width": "100%", + "height": "100%", + "tab_pos": "top", + "items": [ + { + "name": "platform_default", + "label": "会话 1", + "removable": false, + "content": { + "widgettype": "VBox", + "options": {"css": "filler", "width": "100%", "height": "100%"}, + "subwidgets": [ + { + "widgettype": "urlwidget", + "options": {"url": "{{entire_url('/pipeline_core/api/agent_menus.dspy')}}?session_id=default_platform_general&pipeline_id=platform_general", "method": "GET"} + }, + { + "widgettype": "AgentIO", + "options": { + "css": "filler", + "margin": "0 24px 24px 24px", + "url": "/pipeline_core/api/agent_chat.dspy?session_id=default_platform_general&pipeline_id=platform_general", + "model_dataurl": "/pipeline_core/api/agent_model_options.dspy?session_id=default_platform_general&pipeline_id=platform_general", + "model_cwidth": 14, + "placeholder": "例如:通读这份文档并配置模型 <文档URL>..." + } + } + ] + } + } + ] + } + } + ] +}