agent-Specialization/modules/review_agent_config.py
JOJO deaa1560a5 feat(personalization): 个人空间支持配置对话标题生成模型
新增 title_model 配置,复用子智能体模型库条目生成对话标题:
- 个人空间「常规」页签新增「标题生成模型」下拉菜单(复用子智能体
  模型列表,general 页签挂载时加载)
- 标题生成改为使用所选子智能体模型 profile;未配置时回落模型库
  default_model,移除 AGENT_TITLE_* 环境变量覆盖与主智能体默认
  模型回退(个人空间为唯一配置来源)
- personalization 白名单注册 title_model 键(默认值 + sanitize)
2026-09-01 09:46:05 +08:00

117 lines
4.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""审核智能体统一配置解析。
三个审核智能体(自动审批 auto_approval / 目标审核 goal_review / 工作流审核 workflow_review
的模型与运行参数统一来自个人空间设置personalization.json 的 review_agents 键):
- model / thinking模型名与思考模式模型条目复用子智能体模型库 sub_agent_models.json
模型名留空时使用模型库的 default_model
- timeout_seconds / max_rounds / max_command_timeout审核请求超时、最大轮次、只读命令超时。
历史上三个智能体各自读取独立的部署级 json 配置auto_approval.json / goal_review.json /
workflow_review.json该方式已彻底废弃不再做任何向后兼容。
"""
import json
from pathlib import Path
from typing import Any, Dict
from config import DATA_DIR
from config.sub_agent import SUB_AGENT_MODELS_CONFIG_FILE
from modules.personalization_manager import REVIEW_AGENT_KEYS
__all__ = ["resolve_review_agent_config", "resolve_sub_agent_model_profile", "REVIEW_AGENT_KEYS"]
def _load_model_entry(model_name: str) -> Dict[str, Any]:
"""从子智能体模型库解析出指定模型的 profile名称留空则用 default_model。
返回 APIClient.apply_profile 格式的 profile含 fast/thinking 两段),失败返回 None。
"""
config_path = Path(SUB_AGENT_MODELS_CONFIG_FILE)
if not config_path.exists():
return None
try:
raw = json.loads(config_path.read_text(encoding="utf-8"))
except Exception:
return None
models = raw.get("models", []) if isinstance(raw, dict) else (raw if isinstance(raw, list) else [])
default_key = str(raw.get("default_model", "")) if isinstance(raw, dict) else ""
from modules.sub_agent.toolkit import _build_sub_agent_profile
model_map: Dict[str, Dict[str, Any]] = {}
for item in models:
if not isinstance(item, dict):
continue
profile = _build_sub_agent_profile(item)
if profile:
model_map[profile["name"]] = profile
chosen = model_name or default_key
if chosen not in model_map and model_map:
chosen = next(iter(model_map))
return model_map.get(chosen)
def resolve_sub_agent_model_profile(model_name: str) -> Dict[str, Any]:
"""按名称从子智能体模型库解析模型 profileAPIClient.apply_profile 格式)。
名称留空或不存在时回落模型库 default_model模型库也不可用时返回 None
由调用方决定兜底行为)。
"""
return _load_model_entry(str(model_name).strip() if model_name and str(model_name).strip() else "")
def resolve_review_agent_config(agent_key: str) -> Dict[str, Any]:
"""解析指定审核智能体的完整运行配置。
返回字段name / url / key / model / extra_params / timeout_seconds / max_rounds /
max_command_timeout。模型未配置或模型库不可用时 url/key/model 为空字符串,
由各审核智能体走既有的「配置缺失」兜底行为。
"""
base: Dict[str, Any] = {
"name": f"{agent_key}-agent",
"url": "",
"key": "",
"model": "",
"extra_params": {},
"timeout_seconds": 60,
"max_rounds": 3,
"max_command_timeout": 60,
}
if agent_key not in REVIEW_AGENT_KEYS:
return base
try:
from modules.personalization_manager import load_personalization_config
personal = load_personalization_config(DATA_DIR)
except Exception:
personal = {}
settings = (personal.get("review_agents") or {}).get(agent_key)
if not isinstance(settings, dict):
return base
base["timeout_seconds"] = int(settings.get("timeout_seconds") or base["timeout_seconds"])
base["max_rounds"] = max(1, int(settings.get("max_rounds") or base["max_rounds"]))
base["max_command_timeout"] = max(1, int(settings.get("max_command_timeout") or base["max_command_timeout"]))
model_name = str(settings.get("model") or "").strip()
profile = _load_model_entry(model_name)
if not profile:
return base
# 按思考模式选段;模型不支持 thinking 时回落 fast 段
thinking = bool(settings.get("thinking"))
segment = profile.get("thinking") if thinking else None
if not segment:
segment = profile.get("fast") or {}
base["url"] = str(segment.get("base_url") or "").strip()
base["key"] = str(segment.get("api_key") or "").strip()
base["model"] = str(segment.get("model_id") or "").strip()
extra = segment.get("extra_params")
base["extra_params"] = dict(extra) if isinstance(extra, dict) else {}
max_tokens = segment.get("max_tokens")
if isinstance(max_tokens, int) and max_tokens > 0 and "max_tokens" not in base["extra_params"]:
base["extra_params"]["max_tokens"] = max_tokens
return base