feat(platform): apply_llm_config自动生成模型配套技能(2026-09-08用户定夺:每个模型加入需配套skill)

- _write_model_skill: skills/models/{model}/SKILL.md,数据源=落库param_schema
  (required必备/非required可选带默认值)+能力字典模态+媒体三数组契约+doc出处
- 幂等:重apply覆盖更新;生成失败只记skill_notes不阻断(门禁降级LLM能力语义→代码兜底)
- 目录约定:skills/models/不在SkillLoader扫描范围,不污染agent技能目录层
This commit is contained in:
yumoqing 2026-09-08 12:31:22 +08:00
parent 7a041cbb18
commit 02650d4c2d

View File

@ -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)