From 5f3b30bb7526d1628f9a7e6ccbaa67aa370febff Mon Sep 17 00:00:00 2001 From: YIN Keting Date: Thu, 17 Sep 2026 09:03:40 +0800 Subject: [PATCH] feat: refresh demo model catalog --- docs/quickstart.md | 2 + docs/quickstart.zh-CN.md | 1 + src/backend/main.py | 57 +++---- src/backend/services/demo_seed.py | 119 +++++++++++-- .../tests/test_agent_type_config_seed.py | 158 +++++++++++++++++- src/backend/tests/test_demo_seed.py | 10 +- 6 files changed, 293 insertions(+), 54 deletions(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index 025e700..4b493c2 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -101,6 +101,8 @@ automatically. 3. **Select Agents** (required): - select at least one agent from the list - pre-seeded demo agents include Claude Max, Codex Pro, and Copilot Pro + - the pre-seeded model catalog is an editable example snapshot; actual model + availability depends on your subscription, client, and organization policy - configure same-machine deployment settings for each agent where needed 4. Click "创建项目". diff --git a/docs/quickstart.zh-CN.md b/docs/quickstart.zh-CN.md index 083ee0b..67596c9 100644 --- a/docs/quickstart.zh-CN.md +++ b/docs/quickstart.zh-CN.md @@ -95,6 +95,7 @@ docker compose ps 3. **选择 Agent**(必需): - 必须从列表中选择至少一个 Agent - 预置演示 Agent 包括 Claude Max、Codex Pro、Copilot Pro + - 预置模型目录是可编辑的示例快照;实际可用模型取决于订阅、客户端和组织策略 - 按需为每个 Agent 配置同机部署设置 4. 点击"创建项目"。 diff --git a/src/backend/main.py b/src/backend/main.py index 81ef3bf..20aacbf 100644 --- a/src/backend/main.py +++ b/src/backend/main.py @@ -12,7 +12,7 @@ from config import DEFAULT_MAX_REVIEW_ROUNDS, settings, validate_security_config from database import engine, SessionLocal, Base -from models import Agent, User, AgentTypeConfig, ModelDefinition, AgentTypeModelMap, Project, ProjectPlan, Task, GlobalSetting, ProcessTemplate +from models import Agent, User, AgentTypeConfig, Project, ProjectPlan, Task, GlobalSetting, ProcessTemplate from auth import hash_password from routers import auth as auth_router from routers import agents as agents_router @@ -27,7 +27,12 @@ from routers import codex_usage as codex_usage_router from services.polling_service import polling_loop from services.prompt_settings import DEFAULT_PLAN_CO_LOCATION_GUIDANCE, PLAN_CO_LOCATION_GUIDANCE_KEY -from services.demo_seed import DEMO_AGENT_TYPE_CATALOG, DEMO_MODEL_CAPABILITIES, seed_demo_project +from services.demo_seed import ( + DEMO_AGENT_TYPE_CATALOG, + ensure_demo_agent_type_catalog, + refresh_legacy_demo_agent_models, + seed_demo_project, +) from services.issue_review_loop import ensure_issue_review_loop_template logging.basicConfig(level=logging.INFO) @@ -210,40 +215,30 @@ def repair_legacy_agent_reset_times(): def seed_agent_type_configs(): - """Seed the default agent type catalog if tables are empty.""" + """Seed or non-destructively refresh the default demo model catalog.""" db = SessionLocal() try: - if db.query(AgentTypeConfig).first() is not None: - return # Already seeded - - model_cache: dict[str, ModelDefinition] = {} - for type_order, type_spec in enumerate(DEMO_AGENT_TYPE_CATALOG): - agent_type = AgentTypeConfig( - name=type_spec["name"], - description=type_spec["description"], - display_order=type_order, + existing_type_names = { + agent_type.name + for agent_type in db.query(AgentTypeConfig).all() + } + default_type_names = {spec["name"] for spec in DEMO_AGENT_TYPE_CATALOG} + create_missing_types = not existing_type_names + managed_default_catalog = create_missing_types or bool( + existing_type_names.intersection(default_type_names) + ) + + refreshed_agents = 0 + if managed_default_catalog: + ensure_demo_agent_type_catalog( + db, + create_missing_types=create_missing_types, ) - db.add(agent_type) - db.flush() - for model_order, model_name in enumerate(type_spec["models"]): - if model_name not in model_cache: - model_def = ModelDefinition( - name=model_name, - capability=DEMO_MODEL_CAPABILITIES.get(model_name), - ) - db.add(model_def) - db.flush() - model_cache[model_name] = model_def - db.add(AgentTypeModelMap( - agent_type_id=agent_type.id, - model_definition_id=model_cache[model_name].id, - display_order=model_order, - )) + refreshed_agents = refresh_legacy_demo_agent_models(db) db.commit() logger.info( - "Seeded agent type configs with %d types and %d models", - len(DEMO_AGENT_TYPE_CATALOG), - len(model_cache), + "Synchronized demo model catalog (%d legacy demo agents refreshed)", + refreshed_agents, ) finally: db.close() diff --git a/src/backend/services/demo_seed.py b/src/backend/services/demo_seed.py index c79e7bc..451c657 100644 --- a/src/backend/services/demo_seed.py +++ b/src/backend/services/demo_seed.py @@ -27,7 +27,20 @@ DEMO_COLLABORATION_DIR = "demo/half-demo-collaboration" DEMO_REPO_URL = "https://github.com/keting/half.git" +# Curated examples, not an exhaustive compatibility list. Availability varies +# by subscription and client. Last reviewed against current product menus on +# 2026-09-16. DEMO_MODEL_CAPABILITIES = { + "Fable 5": "高判断密度任务:架构取舍、复杂需求澄清、严格评审、疑难排障与重要统稿。", + "Opus 5 (1M)": "超长上下文任务:跨大量文件或长文档分析、复杂仓库规划与一致性检查。", + "Sonnet 5": "日常编码主力:范围明确的实现、测试补全、代码审查与技术文档。", + "gpt-5.6-sol": "复杂或高风险 coding:长程规划、工具协调、跨文件实现、验证与终验。", + "gpt-5.6-terra": "日常工程任务:仓库探索、常规实现、代码审查与文档维护,兼顾速度和质量。", + "gpt-5.6-luna": "目标清晰、可批量验收的任务:分类、提取、格式修补、测试生成与后台自动化。", + "Gemini 3.6 Flash": "快速轻量任务:代码解释、初步排查、短反馈循环与辅助审查。", +} + +LEGACY_DEMO_MODEL_CAPABILITIES = { "gpt-5.5": ( "优先处理高复杂度、强推理、长链路的专业工作,如大型代码库改造、跨系统问题排查、" "深度研究分析、复杂产品/架构决策与端到端交付;相比 GPT-5.4,它更适合作为高难度任务的主代理、" @@ -41,9 +54,7 @@ "派给最难任务:跨文档需求澄清、关键架构取舍、复杂重构、疑难排障、严格代码审查、" "深度论文评审;强项是长链路规划、大代码库可靠性和自纠错,不要浪费在常规实现上。" ), - "Opus 4.6": ( - "适合高难度代码审查、复杂需求澄清、架构判断和跨文件改造;可作为质量把关和深度评审模型。" - ), + "Opus 4.6": "适合高难度代码审查、复杂需求澄清、架构判断和跨文件改造;可作为质量把关和深度评审模型。", "Sonnet 4.6": ( "默认主力模型;适合大多数生产任务,尤其是需求细化、设计展开、业务代码实现、测试补全、" "技术文档和知识工作;当效果接近旗舰时,优先用它以换取更好成本/速度平衡。" @@ -55,8 +66,8 @@ "name": "Claude Max", "slug": "claude-max", "agent_type": "claude-max", - "model_name": "Opus 4.7", - "models": ["Opus 4.7", "Sonnet 4.6"], + "model_name": "Fable 5", + "models": ["Fable 5", "Opus 5 (1M)", "Sonnet 5"], "co_located": False, "display_order": 1, }, @@ -64,8 +75,8 @@ "name": "Codex Pro", "slug": "codex-pro", "agent_type": "chatgpt-pro", - "model_name": "gpt-5.5", - "models": ["gpt-5.5", "gpt-5.4"], + "model_name": "gpt-5.6-sol", + "models": ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"], "co_located": True, "display_order": 2, }, @@ -73,8 +84,8 @@ "name": "Copilot Pro", "slug": "copilot-pro", "agent_type": "copilot-pro", - "model_name": "Opus 4.6", - "models": ["Opus 4.6", "gpt-5.4", "Sonnet 4.6", "Opus 4.7"], + "model_name": "Sonnet 5", + "models": ["Sonnet 5", "gpt-5.6-sol", "Gemini 3.6 Flash"], "co_located": False, "display_order": 3, }, @@ -84,20 +95,35 @@ { "name": "claude-max", "description": "Claude Max subscription agent for deep review, architecture, and complex reasoning tasks.", - "models": ["Opus 4.7", "Sonnet 4.6"], + "models": ["Fable 5", "Opus 5 (1M)", "Sonnet 5"], }, { "name": "chatgpt-pro", "description": "ChatGPT Pro agent for high-complexity implementation, planning, and end-to-end delivery.", - "models": ["gpt-5.5", "gpt-5.4"], + "models": ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"], }, { "name": "copilot-pro", "description": "Copilot Pro agent for implementation support, testing, and code review workflows.", - "models": ["Opus 4.6", "gpt-5.4", "Sonnet 4.6", "Opus 4.7"], + "models": ["Sonnet 5", "gpt-5.6-sol", "Gemini 3.6 Flash"], }, ] +LEGACY_DEMO_AGENT_MODELS = { + "claude-max": {"model_name": "Opus 4.7", "models": ["Opus 4.7", "Sonnet 4.6"]}, + "codex-pro": {"model_name": "gpt-5.5", "models": ["gpt-5.5", "gpt-5.4"]}, + "copilot-pro": { + "model_name": "Opus 4.6", + "models": ["Opus 4.6", "gpt-5.4", "Sonnet 4.6", "Opus 4.7"], + }, +} + +LEGACY_DEMO_TYPE_MODELS = { + "claude-max": ["Opus 4.7", "Sonnet 4.6"], + "chatgpt-pro": ["gpt-5.5", "gpt-5.4"], + "copilot-pro": ["Opus 4.6", "gpt-5.4", "Sonnet 4.6", "Opus 4.7"], +} + LEGACY_DEFAULT_AGENT_TYPES = {"claude", "codex", "cursor", "windsurf"} DEMO_AGENT_ROLES = { @@ -219,7 +245,8 @@ def _ensure_agent(db: Session, admin: User, spec: dict) -> Agent: return agent -def _ensure_agent_type_catalog(db: Session) -> None: +def ensure_demo_agent_type_catalog(db: Session, *, create_missing_types: bool = True) -> None: + """Add current demo models without deleting user-defined catalog entries.""" max_order = db.query(AgentTypeConfig.display_order).order_by( AgentTypeConfig.display_order.desc(), AgentTypeConfig.id.desc(), @@ -229,6 +256,8 @@ def _ensure_agent_type_catalog(db: Session) -> None: for type_spec in DEMO_AGENT_TYPE_CATALOG: agent_type = db.query(AgentTypeConfig).filter(AgentTypeConfig.name == type_spec["name"]).first() if agent_type is None: + if not create_missing_types: + continue agent_type = AgentTypeConfig( name=type_spec["name"], description=type_spec["description"], @@ -240,6 +269,30 @@ def _ensure_agent_type_catalog(db: Session) -> None: elif not agent_type.description: agent_type.description = type_spec["description"] + existing_maps = db.query(AgentTypeModelMap).filter( + AgentTypeModelMap.agent_type_id == agent_type.id + ).order_by(AgentTypeModelMap.display_order, AgentTypeModelMap.id).all() + existing_model_ids = [mapping.model_definition_id for mapping in existing_maps] + models_by_id = ( + { + model.id: model.name + for model in db.query(ModelDefinition).filter(ModelDefinition.id.in_(existing_model_ids)).all() + } + if existing_model_ids + else {} + ) + existing_names = [models_by_id[model_id] for model_id in existing_model_ids if model_id in models_by_id] + legacy_names = LEGACY_DEMO_TYPE_MODELS.get(type_spec["name"], []) + legacy_default_catalog = existing_names == legacy_names + + if legacy_default_catalog: + for mapping in existing_maps: + mapping.display_order += len(type_spec["models"]) + + next_model_order = max( + (mapping.display_order for mapping in existing_maps), + default=-1, + ) + 1 for model_order, model_name in enumerate(type_spec["models"]): model_def = db.query(ModelDefinition).filter(ModelDefinition.name == model_name).first() if model_def is None: @@ -260,8 +313,44 @@ def _ensure_agent_type_catalog(db: Session) -> None: db.add(AgentTypeModelMap( agent_type_id=agent_type.id, model_definition_id=model_def.id, - display_order=model_order, + display_order=model_order if legacy_default_catalog or not existing_maps else next_model_order, )) + if not legacy_default_catalog and existing_maps: + next_model_order += 1 + + +def refresh_legacy_demo_agent_models(db: Session) -> int: + """Refresh untouched v1 demo Agents while preserving user edits.""" + specs_by_slug = {spec["slug"]: spec for spec in DEMO_AGENTS} + updated = 0 + + for slug, legacy in LEGACY_DEMO_AGENT_MODELS.items(): + agent = db.query(Agent).filter(Agent.slug == slug).first() + if agent is None or agent.model_name != legacy["model_name"]: + continue + try: + stored_models = json.loads(agent.models_json or "[]") + except json.JSONDecodeError: + continue + legacy_models = [ + { + "model_name": model_name, + "capability": LEGACY_DEMO_MODEL_CAPABILITIES[model_name], + } + for model_name in legacy["models"] + ] + legacy_capability = ";".join(item["capability"] for item in legacy_models) + if stored_models != legacy_models or agent.capability != legacy_capability: + continue + + current = specs_by_slug[slug] + current_models = _model_entries(current["models"]) + agent.model_name = current["model_name"] + agent.models_json = json.dumps(current_models, ensure_ascii=False) + agent.capability = ";".join(item["capability"] for item in current_models) + updated += 1 + + return updated def _prune_unused_legacy_default_agent_types(db: Session) -> None: @@ -394,7 +483,7 @@ def seed_demo_project(db: Session, admin: User) -> bool: spec["slug"]: _ensure_agent(db, admin, spec) for spec in DEMO_AGENTS } - _ensure_agent_type_catalog(db) + ensure_demo_agent_type_catalog(db) _prune_unused_legacy_default_agent_types(db) template = _ensure_template(db, admin) db.flush() diff --git a/src/backend/tests/test_agent_type_config_seed.py b/src/backend/tests/test_agent_type_config_seed.py index b3fec78..51530a6 100644 --- a/src/backend/tests/test_agent_type_config_seed.py +++ b/src/backend/tests/test_agent_type_config_seed.py @@ -1,3 +1,4 @@ +import json import sys import unittest from pathlib import Path @@ -12,7 +13,8 @@ sys.path.insert(0, str(BACKEND_DIR)) from main import seed_agent_type_configs -from models import AgentTypeConfig, AgentTypeModelMap, Base, ModelDefinition +from models import Agent, AgentTypeConfig, AgentTypeModelMap, Base, ModelDefinition +from services.demo_seed import LEGACY_DEMO_MODEL_CAPABILITIES class AgentTypeConfigSeedTests(unittest.TestCase): @@ -28,6 +30,7 @@ def setUp(self): def test_seed_uses_current_demo_agent_catalog(self): with patch("main.SessionLocal", self.SessionLocal): seed_agent_type_configs() + seed_agent_type_configs() db = self.SessionLocal() try: @@ -44,11 +47,158 @@ def test_seed_uses_current_demo_agent_catalog(self): models_by_name = {model.name: model for model in db.query(ModelDefinition).all()} self.assertEqual( set(models_by_name), - {"Opus 4.7", "Sonnet 4.6", "gpt-5.5", "gpt-5.4", "Opus 4.6"}, + { + "Fable 5", + "Opus 5 (1M)", + "Sonnet 5", + "gpt-5.6-sol", + "gpt-5.6-terra", + "gpt-5.6-luna", + "Gemini 3.6 Flash", + }, + ) + self.assertTrue(models_by_name["gpt-5.6-sol"].capability) + + self.assertEqual(db.query(AgentTypeModelMap).count(), 9) + finally: + db.close() + + def test_seed_preserves_modified_demo_agent_selection(self): + db = self.SessionLocal() + try: + db.add(AgentTypeConfig(name="chatgpt-pro")) + db.add(Agent( + name="My Codex", + slug="codex-pro", + agent_type="chatgpt-pro", + model_name="custom-model", + models_json=json.dumps([ + {"model_name": "custom-model", "capability": "User choice"}, + ]), + )) + db.commit() + finally: + db.close() + + with patch("main.SessionLocal", self.SessionLocal): + seed_agent_type_configs() + + db = self.SessionLocal() + try: + agent = db.query(Agent).filter(Agent.slug == "codex-pro").one() + self.assertEqual(agent.model_name, "custom-model") + self.assertEqual( + [item["model_name"] for item in json.loads(agent.models_json)], + ["custom-model"], + ) + finally: + db.close() + + def test_seed_preserves_modified_legacy_demo_capabilities(self): + db = self.SessionLocal() + try: + db.add(AgentTypeConfig(name="chatgpt-pro")) + db.add(Agent( + name="Codex Pro", + slug="codex-pro", + agent_type="chatgpt-pro", + model_name="gpt-5.5", + models_json=json.dumps([ + {"model_name": "gpt-5.5", "capability": "User-edited capability"}, + { + "model_name": "gpt-5.4", + "capability": LEGACY_DEMO_MODEL_CAPABILITIES["gpt-5.4"], + }, + ]), + capability="User-edited capability", + )) + db.commit() + finally: + db.close() + + with patch("main.SessionLocal", self.SessionLocal): + seed_agent_type_configs() + + db = self.SessionLocal() + try: + agent = db.query(Agent).filter(Agent.slug == "codex-pro").one() + self.assertEqual(agent.model_name, "gpt-5.5") + self.assertEqual( + json.loads(agent.models_json)[0]["capability"], + "User-edited capability", ) - self.assertTrue(models_by_name["gpt-5.5"].capability) + finally: + db.close() - self.assertEqual(db.query(AgentTypeModelMap).count(), 8) + def test_seed_refreshes_legacy_defaults_without_removing_old_models(self): + db = self.SessionLocal() + try: + agent_type = AgentTypeConfig(name="chatgpt-pro", description="User description") + db.add(agent_type) + db.flush() + for index, model_name in enumerate(["gpt-5.5", "gpt-5.4"]): + model = ModelDefinition( + name=model_name, + capability=LEGACY_DEMO_MODEL_CAPABILITIES[model_name], + ) + db.add(model) + db.flush() + db.add(AgentTypeModelMap( + agent_type_id=agent_type.id, + model_definition_id=model.id, + display_order=index, + )) + db.add(Agent( + name="Codex Pro", + slug="codex-pro", + agent_type="chatgpt-pro", + model_name="gpt-5.5", + models_json=json.dumps([ + { + "model_name": "gpt-5.5", + "capability": LEGACY_DEMO_MODEL_CAPABILITIES["gpt-5.5"], + }, + { + "model_name": "gpt-5.4", + "capability": LEGACY_DEMO_MODEL_CAPABILITIES["gpt-5.4"], + }, + ]), + capability=";".join([ + LEGACY_DEMO_MODEL_CAPABILITIES["gpt-5.5"], + LEGACY_DEMO_MODEL_CAPABILITIES["gpt-5.4"], + ]), + )) + db.commit() + finally: + db.close() + + with patch("main.SessionLocal", self.SessionLocal): + seed_agent_type_configs() + + db = self.SessionLocal() + try: + type_names = {agent_type.name for agent_type in db.query(AgentTypeConfig).all()} + self.assertEqual(type_names, {"chatgpt-pro"}) + + agent_type = db.query(AgentTypeConfig).filter(AgentTypeConfig.name == "chatgpt-pro").one() + mappings = db.query(AgentTypeModelMap).filter( + AgentTypeModelMap.agent_type_id == agent_type.id + ).order_by(AgentTypeModelMap.display_order, AgentTypeModelMap.id).all() + model_names_by_id = {model.id: model.name for model in db.query(ModelDefinition).all()} + self.assertEqual( + [model_names_by_id[mapping.model_definition_id] for mapping in mappings], + ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5", "gpt-5.4"], + ) + + agent = db.query(Agent).filter(Agent.slug == "codex-pro").one() + self.assertEqual(agent.model_name, "gpt-5.6-sol") + self.assertEqual( + [item["model_name"] for item in json.loads(agent.models_json)], + ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"], + ) + self.assertEqual(agent_type.description, "User description") + self.assertIn("gpt-5.5", model_names_by_id.values()) + self.assertIn("gpt-5.4", model_names_by_id.values()) finally: db.close() diff --git a/src/backend/tests/test_demo_seed.py b/src/backend/tests/test_demo_seed.py index 10febdb..ddf9522 100644 --- a/src/backend/tests/test_demo_seed.py +++ b/src/backend/tests/test_demo_seed.py @@ -73,7 +73,9 @@ def test_seed_creates_browsable_demo_project(self): agents = {agent.slug: agent for agent in self.db.query(Agent).all()} self.assertEqual(set(agents), {"claude-max", "codex-pro", "copilot-pro"}) - self.assertEqual(agents["codex-pro"].model_name, "gpt-5.5") + self.assertEqual(agents["claude-max"].model_name, "Fable 5") + self.assertEqual(agents["codex-pro"].model_name, "gpt-5.6-sol") + self.assertEqual(agents["copilot-pro"].model_name, "Sonnet 5") agent_types = { agent_type.name: agent_type @@ -90,9 +92,9 @@ def type_models(type_name: str) -> list[str]: ).order_by(AgentTypeModelMap.display_order, AgentTypeModelMap.id).all() return [models_by_id[mapping.model_definition_id] for mapping in maps] - self.assertEqual(type_models("claude-max"), ["Opus 4.7", "Sonnet 4.6"]) - self.assertEqual(type_models("chatgpt-pro"), ["gpt-5.5", "gpt-5.4"]) - self.assertEqual(type_models("copilot-pro"), ["Opus 4.6", "gpt-5.4", "Sonnet 4.6", "Opus 4.7"]) + self.assertEqual(type_models("claude-max"), ["Fable 5", "Opus 5 (1M)", "Sonnet 5"]) + self.assertEqual(type_models("chatgpt-pro"), ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]) + self.assertEqual(type_models("copilot-pro"), ["Sonnet 5", "gpt-5.6-sol", "Gemini 3.6 Flash"]) def test_seed_prunes_unused_legacy_default_agent_types_from_demo_catalog(self): self.db.add_all([