diff --git a/pipeline_platform/platform_ability.py b/pipeline_platform/platform_ability.py index 4f29e79..2f27b2d 100644 --- a/pipeline_platform/platform_ability.py +++ b/pipeline_platform/platform_ability.py @@ -607,6 +607,133 @@ def _capability_guard(capability, description): return None, None +# ────────────────────── 模型配套技能自动生成(2026-09-08 用户定夺) ────────────────────── + +def _model_skill_path(model_name): + """模型配套技能路径:skills/models/{model}/SKILL.md(全局模板目录)。 + + skills/models/ 不在 SkillLoader 扫描范围(global/pipelines/orgs/users), + 不进 agent 技能目录层——它是 invoke_model 完备性裁决的数据契约, + 由 platform_model_tools.load_model_skill 直读。 + """ + import os + from pipeline_core.skill_pack import get_skills_base + safe = "".join(ch for ch in (model_name or "") if ch.isalnum() or ch in "-_.") + if not safe: + raise ValueError("模型名不能用于目录: %r" % model_name) + return os.path.join(get_skills_base(), "models", safe, "SKILL.md") + + +_CAP_MODAL_TEXT = { + "t2t": ("文本", "文本"), "i2t": ("图像", "文本"), "m2t": ("多媒体", "文本"), + "t2i": ("文本", "图像"), "i2i": ("图像", "图像"), "t2v": ("文本", "视频"), + "i2v": ("图像", "视频"), "r2v": ("参考媒体(图像/视频/音频组合)", "视频"), + "tts": ("文本", "语音"), "asr": ("语音", "文本"), + "embedding": ("文本", "向量"), "rerank": ("候选集", "排序"), +} + + +async def _write_model_skill(sor, vmid, m, spec, profile_ids): + """生成/更新模型配套技能(幂等:重 apply 覆盖旧版)。 + + 内容全部来自本次落库的确定数据(不编造): + - 能力模态(输入→输出,能力字典语义) + - 必备输入(param_schema 里 required=true 的字段 + 能力模态决定的媒体输入) + - 可选参数(required=false,带默认值) + - 调用契约(三数组媒体格式 + 产物形态) + - 出处(doc_url / vendor / sync_mode) + invoke_model 的完备性裁决 LLM 按本技能对照用户已给内容判断缺什么。 + """ + import os + cap = (m.get("capability") or "t2t").strip().lower() + inp, outp = _CAP_MODAL_TEXT.get(cap, ("文本", "产物")) + desc = (m.get("description") or "").strip() + sync_mode = "async" if str(m.get("sync_mode") or "").strip() == "async" else "sync" + doc_url = (spec.get("doc_url") or "").strip() + + # param_schema(本次生成/复用的模板)→ 必备/可选参数表 + required_rows, optional_rows = [], [] + pid = profile_ids.get(cap, "") + if pid: + recs = await sor.sqlExe( + "SELECT param_schema FROM llm_api_profile WHERE id=${i}$ LIMIT 1", {"i": pid}) + await sor.sqlExe("COMMIT", {}) + if recs: + try: + schema = json.loads(getattr(recs[0], "param_schema", "") or "[]") + except Exception: + schema = [] + for f in schema if isinstance(schema, list) else []: + if not isinstance(f, dict): + continue + nm = f.get("name") or "" + label = f.get("label") or nm + dv = f.get("defaultvalue") + if f.get("required"): + required_rows.append("- %s(%s)" % (nm, label)) + else: + optional_rows.append("- %s:%s%s" % ( + nm, label, (",默认 %s" % dv) if dv not in (None, "") else "")) + + # 能力模态决定的媒体输入(媒体三数组契约) + media_required = [] + if cap in ("i2v", "i2i"): + media_required.append("- image_files:输入图片(公网 URL 或 base64,字符串或数组)——必备") + elif cap == "asr": + media_required.append("- audio_files:输入音频(公网 URL 或 base64)——必备") + elif cap == "r2v": + media_required.append("- image_files / audio_files / video_files:至少一类参考素材——必备") + elif cap in ("t2v",): + media_required.append("- image_files:(可选)提供参考图则接近 i2v 形态") + + lines = [ + "---", + "name: model-%s" % vmid, + "description: 模型 %s(%s)的输入契约——必备输入/可选参数/媒体格式。invoke_model 完备性裁决依据。" % (vmid, cap), + "capability: %s" % cap, + "sync_mode: %s" % sync_mode, + "---" , + "", + "# 模型 %s 输入契约" % vmid, + "", + "能力:%s(%s → %s)%s" % (cap, inp, outp, ",异步任务(提交→轮询)" if sync_mode == "async" else ",同步返回"), + ] + if desc: + lines += ["", "描述:" + desc[:300]] + lines += ["", "## 必备输入(缺任一即不可调用,须先向用户索取)"] + if media_required: + lines += media_required + if required_rows: + lines += required_rows + if not media_required and not required_rows: + lines.append("- task:任务描述/提示词(%s内容)" % inp) + lines += ["", "## 可选参数(有默认值或缺省可接受,缺失**不算**不完备)"] + lines += (optional_rows or ["- (无)"]) + lines += [ + "", + "## 调用契约", + "- 提示词经 task 传入(组包为 messages 末条文本)。", + "- 媒体输入一律三数组:image_files / audio_files / video_files(值为公网 URL 或 base64 的字符串或数组)。", + "- 产物:%s(返回本地持久 URL)。" % outp, + ] + if doc_url: + lines.append("- 文档出处:" + doc_url) + lines += [ + "", + "## 完备性裁决规则(judge_completeness 按此执行)", + "- 对照「必备输入」逐项检查用户已提供的内容(含任务描述里隐含的 URL/素材)。", + "- 必备项缺失 → 判不完备,追问话术说清缺什么、要什么格式。", + "- 可选参数缺失不拦。判断不了放行(宁可放行不误拦)。", + "", + "(本技能由模型上线 apply_llm_config 自动生成,重 apply 时覆盖更新;数据源=落库的 param_schema+能力字典,人工可修订。)", + ] + path = _model_skill_path(vmid) + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w", encoding="utf-8") as f: + f.write("\n".join(lines)) + logger.info("模型配套技能已写入: %s", path) + + async def _h_apply_llm_config(sor, params, ctx): """按提取的规格写库:供应商(复用/新建)+ 适配模板 + 模型。 @@ -978,6 +1105,23 @@ async def _h_apply_llm_config(sor, params, ctx): "已自动补绑为 %s" % (getattr(acc, "name", ""), bound, protocol, newb)) audit_note = await _audit_media_convention(sor, list(profile_ids.values())) + # 5. 模型配套技能自动生成(2026-09-08 用户定夺:每个模型的加入需要有 + # 配套 skill——记录该模型需要什么样的输入;invoke_model 完备性判断由 + # LLM 按此技能对照用户已给内容裁决缺什么,不再靠硬编码能力表)。 + # 数据全部来自本次落库结果(param_schema 的 required + 能力模态 + + # 媒体契约),生成失败只记 note 不阻断(技能缺失时门禁降级为 + # 「LLM 按能力语义判断 → 代码表兜底」,功能不失效)。 + skill_notes = [] + for m in models: + vmid = (m.get("vendor_model_id") or "").strip() + if not vmid: + continue + try: + await _write_model_skill(sor, vmid, m, spec, profile_ids) + skill_notes.append("%s: 配套技能已生成/更新" % vmid) + except Exception as e: + skill_notes.append("%s: 配套技能生成失败(%s)——完备性判断将按能力语义降级" + % (vmid, str(e)[:80])) return json.dumps({ "vendor": vendor_action, "profiles": profile_ids, "models_created": created, "models_updated": updated, "models_skipped": skipped, @@ -985,6 +1129,7 @@ async def _h_apply_llm_config(sor, params, ctx): "template_errors": tpl_errors, "template_notes": tpl_notes, "account_notes": account_notes, + "skill_notes": skill_notes, "runtime_note": rt_note, "media_audit": audit_note, }, ensure_ascii=False)