feat: 项目-角色-模型配置 + 角色agent模型解析链

- sd_projects 加 default_model 字段(项目缺省模型=创建时的会话模型)
- _t_create_project 记录 created_by + default_model
- _resolve_llm_context 解析链:显式→项目-角色-模型(sd_project_role_models)
  →项目缺省模型(default_model)→RoleSpec→pipelines.default_model→deepseek-v4-pro
- 新增工具 list_role_models / set_role_model(项目设置界面后端)
This commit is contained in:
ymq 2026-08-22 10:19:50 +08:00
parent c50a5b9914
commit 14dca0cc43
3 changed files with 78 additions and 8 deletions

View File

@ -99,28 +99,47 @@ async def _resolve_role(project_id, role):
async def _resolve_llm_context(sor, project_id, role, model_name=None):
"""解析角色 agent 的 LLM 上下文:返回 (model_name, org_id)。
model_name 解析链与会话 agent load_agent_config 一致
model_name 解析链
1. 显式传入的 model_name
2. RoleSpec.model_name角色专属模型
3. 产线 default_modelpipelines.default_model
4. 全局默认 "deepseek-v4-pro"
2. 项目-角色-模型sd_project_role_models设置界面配的
3. 用户当前模型项目创建者 created_by pipeline_agent_settings.default_llm_id llm.name缺省
4. RoleSpec.model_name角色专属模型
5. 产线 default_modelpipelines.default_model
6. 全局默认 "deepseek-v4-pro"
org_id sd_projects.org_id 传给 llm_bridge 做多租户隔离
llm 表查询只取本机构 + 系统级 org_id='0'的模型
"""
org_id = ""
pipeline_id = ""
default_model = ""
try:
recs = await sor.sqlExe(
"SELECT org_id, pipeline_id FROM sd_projects WHERE id=${pid}$", {"pid": project_id})
"SELECT org_id, pipeline_id, default_model FROM sd_projects WHERE id=${pid}$", {"pid": project_id})
if recs:
org_id = getattr(recs[0], "org_id", "") or ""
pipeline_id = getattr(recs[0], "pipeline_id", "") or ""
default_model = getattr(recs[0], "default_model", "") or ""
except Exception:
pass
# 2. 项目-角色-模型(设置界面配的,最高优先)
if not model_name and project_id:
try:
recs = await sor.sqlExe(
"SELECT model_name FROM sd_project_role_models WHERE project_id=${pid}$ AND role=${r}$",
{"pid": project_id, "r": role})
if recs and getattr(recs[0], "model_name", ""):
model_name = getattr(recs[0], "model_name", "")
except Exception:
pass
# 3. 项目缺省模型创建项目时记录的「当前会话模型」session 级缺省)
if not model_name and default_model:
model_name = default_model
if not model_name:
# 2. RoleSpec.model_name角色专属模型
# 4. RoleSpec.model_name角色专属模型
try:
from pipeline_core import get_role_spec
spec = get_role_spec(pipeline_id or await _resolve_pipeline_id(project_id), role)
@ -129,7 +148,7 @@ async def _resolve_llm_context(sor, project_id, role, model_name=None):
except Exception:
pass
if not model_name and pipeline_id:
# 3. 产线 default_model
# 5. 产线 default_model
try:
recs = await sor.sqlExe(
"SELECT default_model FROM pipelines WHERE id=${pid}$", {"pid": pipeline_id})
@ -138,7 +157,7 @@ async def _resolve_llm_context(sor, project_id, role, model_name=None):
except Exception:
pass
if not model_name:
# 4. 全局默认
# 6. 全局默认
model_name = "deepseek-v4-pro"
return model_name, org_id

View File

@ -860,6 +860,8 @@ class AgentExecutor:
"id": pid_val, "name": name, "description": desc,
"status": "active", "org_id": org_id,
"pipeline_id": self.space, "workspace_dir": workspace_dir,
"created_by": self.user_id or "",
"default_model": self.model_name or "",
})
await sor.C("sd_iterations", {
"id": getID(), "project_id": pid_val,

View File

@ -178,6 +178,8 @@ SDL_TOOLS = [
),
# ── 项目生命周期 ──
ToolDefinition(name="list_projects", description="列出项目列表(含状态)", parameters={"status": "按状态筛选(可选)"}, category="project"),
ToolDefinition(name="list_role_models", description="列出项目各角色的模型配置(缺省用项目缺省模型)", parameters={"project_id": "项目ID"}, category="project"),
ToolDefinition(name="set_role_model", description="设置某角色的模型(如 deploy_test 用 qwen3.8-maxmodel_name 空=清除回退缺省)", parameters={"project_id": "项目ID", "role": "角色名(agent.develop/agent.deploy_test等)", "model_name": "模型名(llm.name空=回退缺省)"}, category="project"),
ToolDefinition(name="start_project", description="启动项目(draft/archived→active)", parameters={"project_id": "项目ID"}, category="project"),
ToolDefinition(name="complete_project", description="完成项目(active→completed)", parameters={"project_id": "项目ID"}, category="project"),
ToolDefinition(name="archive_project", description="归档项目(completed→archived)", parameters={"project_id": "项目ID"}, category="project"),
@ -1168,6 +1170,51 @@ async def _h_start_project(sor, p, ctx):
return f"OK: {msg}" if ok else f"ERROR: {msg}"
async def _h_list_role_models(sor, p, ctx):
"""列出项目各角色的模型配置sd_project_role_models"""
pid = (p.get("project_id", "") or "").strip() or (ctx.get("project_id", "") or "")
if not pid:
return "需要 project_id"
pid = await _resolve_id(sor, "sd_projects", pid) or pid
recs = await sor.sqlExe(
"SELECT role, model_name FROM sd_project_role_models WHERE project_id=${pid}$ ORDER BY role",
{"pid": pid})
await sor.sqlExe("COMMIT", {})
if not recs:
return "(无角色模型配置,所有角色用项目缺省模型)"
lines = []
for r in recs:
lines.append(f"- {r.role} -> {r.model_name or '(空=缺省)'}")
return "\n".join(lines)
async def _h_set_role_model(sor, p, ctx):
"""设置某角色的模型model_name 空 = 清除,回退项目缺省模型)。"""
pid = (p.get("project_id", "") or "").strip() or (ctx.get("project_id", "") or "")
role = (p.get("role", "") or "").strip()
model_name = (p.get("model_name", "") or "").strip()
if not pid:
return "需要 project_id"
if not role:
return "需要 role"
pid = await _resolve_id(sor, "sd_projects", pid) or pid
from appPublic.uniqueID import getID
recs = await sor.sqlExe(
"SELECT id FROM sd_project_role_models WHERE project_id=${pid}$ AND role=${r}$",
{"pid": pid, "r": role})
await sor.sqlExe("COMMIT", {})
if recs:
await sor.sqlExe(
"UPDATE sd_project_role_models SET model_name=${m}$, updated_at=NOW() WHERE id=${id}$",
{"m": model_name or None, "id": getattr(recs[0], "id", "")})
else:
await sor.C("sd_project_role_models", {
"id": getID(), "project_id": pid, "role": role, "model_name": model_name or None,
})
await sor.sqlExe("COMMIT", {})
return f"OK: {role} 模型 -> {model_name or '(空=用项目缺省模型)'}"
async def _h_complete_project(sor, p, ctx):
pid = (p.get("project_id", "") or "").strip()
if not pid:
@ -1760,6 +1807,8 @@ SDL_HANDLERS = {
"verify_feature": _h_verify_feature,
# ── 项目生命周期 ──
"list_projects": _h_list_projects,
"list_role_models": _h_list_role_models,
"set_role_model": _h_set_role_model,
"start_project": _h_start_project,
"complete_project": _h_complete_project,
"archive_project": _h_archive_project,