From 8bb9a0c682f5e76df4b66d9fa251f6a2a7514e39 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Fri, 4 Sep 2026 14:00:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(platform):=20=E6=8F=90=E5=8F=96prompt?= =?UTF-8?q?=E8=A1=A5async=5Fsteps/sync=5Fmode;apply=5Fllm=5Fconfig?= =?UTF-8?q?=E5=86=99profile=E7=9A=84path/headers=E5=88=97+=E5=BC=82?= =?UTF-8?q?=E6=AD=A5=E6=A8=A1=E5=9E=8B=E5=BB=BA=E5=90=8E=E7=BB=AD=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4=E6=A8=A1=E6=9D=BF=E9=93=BE=E7=99=BB=E8=AE=B0query=5Fp?= =?UTF-8?q?rofile=5Fids?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pipeline_platform/platform_ability.py | 88 ++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 16 deletions(-) diff --git a/pipeline_platform/platform_ability.py b/pipeline_platform/platform_ability.py index ade9a13..8ced0fe 100644 --- a/pipeline_platform/platform_ability.py +++ b/pipeline_platform/platform_ability.py @@ -20,6 +20,8 @@ import json import logging import re +from appPublic.uniqueID import getID + from pipeline_core import ( ToolDefinition, PipelineAbility, @@ -223,13 +225,20 @@ _EXTRACT_PROMPT = """你是大模型 API 配置专家。通读下面这份模型 "auth_header": "认证头格式说明(如 Bearer API_KEY)", "request_fields": ["请求体字段名列表"], "response_format": "响应格式说明(content 字段路径 + usage 字段路径)", + "async_steps": [{"purpose": "query|download", "path": "该步骤接口路径", "method": "GET|POST", "request_fields": ["请求字段名列表"], "response_format": "该步骤响应格式说明"}], "models": [{"vendor_model_id": "供应商侧模型ID", "capability": "t2t|t2i|i2t|t2v|embedding|rerank|tts|asr", + "sync_mode": "sync|async(提交后需轮询查询结果的填 async)", "price_input": 输入价元/千token或null, "price_output": 输出价元/千token或null, "cost_input": 输入成本元/千token或null, "cost_output": 输出成本元/千token或null, "description": "一句话说明"}], "doc_notes": "文档中影响配置的关键注意点" } +异步模型规则: +- 文档描述「先提交任务、再轮询查询结果」的模型,sync_mode 填 async,并提取 async_steps: + 至少一条 purpose=query(查询任务状态/结果);若文档另有独立下载/取文件接口,再加一条 purpose=download。 +- 同步模型 async_steps 填空数组 []。 + 定价规则: - 价格按文档原文记录,并保留文档标注的单位(在 description 中写明,如「元/百万tokens」) - 禁止自行换算、推导或统一单位——单位是否吻合由上线测试阶段的记账核对验证 @@ -364,7 +373,9 @@ async def _h_apply_llm_config(sor, params, ctx): 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, + "path": req_tpl["path"], + "headers": json.dumps(req_tpl["headers"], ensure_ascii=False), + "request_template": req_tpl["req"], "response_template": req_tpl["resp"], "param_schema": json.dumps([{"name": "prompt", "label": "提示词", "uitype": "textarea", "required": True}], ensure_ascii=False), @@ -379,6 +390,7 @@ async def _h_apply_llm_config(sor, params, ctx): skipped.append("(缺 vendor_model_id)") continue cap = m.get("capability") or "t2t" + sync_mode = "async" if str(m.get("sync_mode") or "").strip() == "async" else "sync" raw_pi, raw_po = m.get("price_input"), m.get("price_output") price_in, price_out = _f(raw_pi), _f(raw_po) cost_in = _f(m.get("cost_input"), price_in) @@ -387,6 +399,17 @@ async def _h_apply_llm_config(sor, params, ctx): # 文档未给价(null)→ 落 0 并显式标注,避免「0=免费」歧义 if raw_pi is None or raw_po is None: desc = "⚠️文档未列定价(暂落0,需人工补录)。" + desc + # 异步模型:后续步骤模板链(提交后顺序执行 query→[download]) + query_ids = [] + if sync_mode == "async": + steps = spec.get("async_steps") or [] + if not any((s.get("purpose") or "") == "query" for s in steps): + desc = "⚠️异步模型但文档未提取到查询步骤(query_profile_ids 需人工补录)。" + desc + for s in steps: + qid = await _ensure_step_profile(sor, protocol, cap, vmid, s, spec) + if qid: + query_ids.append(qid) + query_ids_json = json.dumps(query_ids) if query_ids else "" recs = await sor.sqlExe( "SELECT id FROM llm_model WHERE vendor_id=${v}$ AND vendor_model_id=${m}$", {"v": vendor_id, "m": vmid}) @@ -395,10 +418,11 @@ async def _h_apply_llm_config(sor, params, ctx): 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() " + "cost_input=${ci}$, cost_output=${co}$, description=${d}$, " + "sync_mode=${sm}$, query_profile_ids=${q}$, updated_at=NOW() " "WHERE id=${i}$", {"pi": price_in, "po": price_out, "ci": cost_in, "co": cost_out, - "d": desc, "i": mid}) + "d": desc, "sm": sync_mode, "q": query_ids_json, "i": mid}) await sor.sqlExe("COMMIT", {}) updated.append(vmid) continue @@ -406,7 +430,8 @@ async def _h_apply_llm_config(sor, params, ctx): 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, ""), + "sync_mode": sync_mode, "profile_id": profile_ids.get(cap, ""), + "query_profile_ids": query_ids_json, "ppid": "", "price_input": price_in, "price_output": price_out, "cost_input": cost_in, "cost_output": cost_out, "default_params": "{}", "status": "active", @@ -424,18 +449,17 @@ async def _h_apply_llm_config(sor, params, ctx): }, ensure_ascii=False) -def _default_templates(protocol: str, capability: str, spec: dict) -> tuple: - """生成适配模板默认值(headers/data/response)。 +def _default_templates(protocol: str, capability: str, spec: dict) -> dict: + """生成适配模板默认值(path/headers 独立列 + data/response 模板)。 OpenAI 兼容协议用平台标准模板;其他协议按文档格式生成骨架供人工微调。 + 返回 {"path", "headers", "req", "resp"}。 """ chat_path = (spec.get("chat_path") or "/chat/completions").strip() + headers = {"Authorization": "Bearer {{api_key}}", + "Content-Type": "application/json"} 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) @@ -444,13 +468,9 @@ def _default_templates(protocol: str, capability: str, spec: dict) -> tuple: "usage": {"prompt_tokens": "usage.prompt_tokens", "completion_tokens": "usage.completion_tokens"}, }, ensure_ascii=False) - return req, resp + return {"path": chat_path, "headers": headers, "req": req, "resp": 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) @@ -458,7 +478,43 @@ def _default_templates(protocol: str, capability: str, spec: dict) -> tuple: "content": spec.get("response_format", "") or "待按文档填写", "__note__": "需人工核对", }, ensure_ascii=False) - return req, resp + return {"path": chat_path, "headers": headers, "req": req, "resp": resp} + + +async def _ensure_step_profile(sor, protocol, cap, vmid, step, spec): + """异步模型的后续步骤模板(query/download):按名幂等创建/复用 profile。 + + step: {"purpose": "query|download", "path", "method", "request_fields", "response_format"} + 返回 profile id。 + """ + purpose = (step.get("purpose") or "query").strip() or "query" + pname = "%s-%s" % (vmid, purpose) + recs = await sor.sqlExe( + "SELECT id FROM llm_api_profile WHERE name=${n}$", {"n": pname}) + await sor.sqlExe("COMMIT", {}) + if recs: + return getattr(recs[0], "id", "") + headers = {"Authorization": "Bearer {{api_key}}", + "Content-Type": "application/json"} + method = (step.get("method") or "POST").strip().upper() or "POST" + req = json.dumps({ + "method": method, + "data": {"__from_doc__": step.get("request_fields") or [], + "__task_id__": "{{task_id}}"}, + "__note__": "异步%s步骤骨架,需按文档人工核对" % purpose, + }, ensure_ascii=False) + resp = json.dumps({ + "content": step.get("response_format", "") or "待按文档填写", + "__note__": "需人工核对", + }, ensure_ascii=False) + pid = getID() + await sor.C("llm_api_profile", { + "id": pid, "name": pname, "protocol": protocol, "capability": cap, + "path": (step.get("path") or "").strip(), + "headers": json.dumps(headers, ensure_ascii=False), + "request_template": req, "response_template": resp, + "param_schema": "", "status": "active"}) + return pid # ────────────────────── 工具 5:模块/应用信息查询 ──────────────────────