From e1e067bac4b8f6c797940f343eda176862163156 Mon Sep 17 00:00:00 2001 From: sinohqb Date: Mon, 3 Aug 2026 01:46:51 +0800 Subject: [PATCH] feat(models): add analysis-default flag for campaign intelligence Introduce ModelPurpose.ANALYSIS and a globally-unique is_analysis_default marker on chat model configs so campaign analysis can resolve its model. Service rejects disabled or non-chat configs; repo clears the previous holder on set. Documented the analysis role in CONTEXT.md. --- .../01-analysis-default-model-config.md | 15 ++++ .../02-campaign-analysis-model-override.md | 14 +++ .../v0.7/issues/03-analysis-agent-and-api.md | 16 ++++ .../04-report-drawer-analysis-section.md | 14 +++ .../05-auto-trigger-and-markdown-export.md | 14 +++ .scratch/v0.7/spec.md | 87 +++++++++++++++++++ CONTEXT.md | 6 +- backend/agenteval/models.py | 1 + backend/agenteval/services/model_configs.py | 15 ++++ backend/agenteval/storage/db.py | 1 + .../storage/model_config_repository.py | 13 +++ backend/agenteval/web/model_config_schemas.py | 3 + frontend/web/src/api.ts | 2 + frontend/web/src/pages/ModelConfigs.tsx | 18 ++++ ...8_add_analysis_default_to_model_configs.py | 28 ++++++ tests/integration/test_model_configs_api.py | 35 ++++++++ tests/unit/test_analysis_default_migration.py | 35 ++++++++ tests/unit/test_model_configs.py | 84 +++++++++++++++++- 18 files changed, 399 insertions(+), 2 deletions(-) create mode 100644 .scratch/v0.7/issues/01-analysis-default-model-config.md create mode 100644 .scratch/v0.7/issues/02-campaign-analysis-model-override.md create mode 100644 .scratch/v0.7/issues/03-analysis-agent-and-api.md create mode 100644 .scratch/v0.7/issues/04-report-drawer-analysis-section.md create mode 100644 .scratch/v0.7/issues/05-auto-trigger-and-markdown-export.md create mode 100644 .scratch/v0.7/spec.md create mode 100644 migrations/versions/a3f9c2e71b48_add_analysis_default_to_model_configs.py create mode 100644 tests/unit/test_analysis_default_migration.py diff --git a/.scratch/v0.7/issues/01-analysis-default-model-config.md b/.scratch/v0.7/issues/01-analysis-default-model-config.md new file mode 100644 index 0000000..516959c --- /dev/null +++ b/.scratch/v0.7/issues/01-analysis-default-model-config.md @@ -0,0 +1,15 @@ +# 01 — 模型配置中心「分析默认」 + +**What to build:** 管理员在模型配置中心能把一个「启用中的 chat 模型」标记为「分析默认」(全局唯一,设新的自动清旧的),API 透出该标记。后端新增 `ModelPurpose.ANALYSIS` 枚举与 `model_configs.is_analysis_default` 列(Alembic 迁移,batch mode,回填 false),服务层校验「停用配置不能设为分析默认」「非 chat 能力不能设为分析默认」。 + +**Blocked by:** None — can start immediately + +**Status:** ready-for-agent + +- [ ] `ModelPurpose` 枚举新增 `ANALYSIS`;CONTEXT.md 的「分析岗位」术语编辑随本 ticket 一并提交 +- [ ] Alembic 迁移:`model_configs` 加 `is_analysis_default` 布尔列(server_default 回填 false) +- [ ] 仓储层设分析默认时互斥清除(参照 `clear_default` 模式);服务层校验:停用 → 拒绝;非 chat 能力 → 拒绝 +- [ ] 创建/更新模型配置的 API 接受并返回 `is_analysis_default` +- [ ] 模型配置中心前端:chat 配置上可设置/展示「分析默认」标记 +- [ ] 单元/集成测试:唯一性互斥、停用拒绝、非 chat 拒绝、API 读写 +- [ ] `pytest` + `ruff` + `npx tsc --noEmit` + `npm run build` 全绿 diff --git a/.scratch/v0.7/issues/02-campaign-analysis-model-override.md b/.scratch/v0.7/issues/02-campaign-analysis-model-override.md new file mode 100644 index 0000000..67038d4 --- /dev/null +++ b/.scratch/v0.7/issues/02-campaign-analysis-model-override.md @@ -0,0 +1,14 @@ +# 02 — 活动级分析模型覆盖 + +**What to build:** 创建评估活动时,表单左列「时间与速度」区新增「分析模型」下拉(选项 = 启用中的 chat 配置,默认项「全局默认(当前默认配置名)」),不选则跟随全局默认。后端 `campaigns` 表加可空 `analysis_model_config_id` 列(迁移),创建 API 接受该字段并校验引用的配置存在(不存在 400),活动详情/列表响应透出该字段。 + +**Blocked by:** 01(「全局默认」语义与下拉数据来自 01 的分析默认标记) + +**Status:** ready-for-agent + +- [ ] Alembic 迁移:`campaigns` 加可空 `analysis_model_config_id` 列 +- [ ] `POST /api/campaigns` 接受 `analysis_model_config_id`(可空);引用不存在的配置返回 400 +- [ ] 活动响应模型透出 `analysis_model_config_id` +- [ ] 创建表单加「分析模型」下拉:默认「全局默认(<名称>)」,选项为启用的 chat 配置;无全局默认时默认项提示「未配置」 +- [ ] 集成测试:带覆盖创建、不带覆盖创建(存 NULL)、无效 id 400 +- [ ] `pytest` + `ruff` + `npx tsc --noEmit` + `npm run build` 全绿 diff --git a/.scratch/v0.7/issues/03-analysis-agent-and-api.md b/.scratch/v0.7/issues/03-analysis-agent-and-api.md new file mode 100644 index 0000000..41cfb42 --- /dev/null +++ b/.scratch/v0.7/issues/03-analysis-agent-and-api.md @@ -0,0 +1,16 @@ +# 03 — 两阶段分析 Agent + 存储 + 分析 API + +**What to build:** 对终态活动可触发生成结构化分析报告。新表 `campaign_analyses`(每活动一行 upsert:status=generating/completed/failed、result JSON、model_config_id 快照、error、triggered_by、时间戳)。新模块 `evaluation/analysis.py`:输入复用 `generate_campaign_report` 聚合结果(不重算,ADR-0002/0004 口径)+ 每场景最多 3 条截断的代表性失败对话;阶段一每场景并行一次 LLM 调用产场景叙述+问题草稿,阶段二汇总产总体结论+跨场景问题+优先级建议;输出 JSON schema 约束,解析失败记 failed;证据 run_id 白名单校验,虚构引用落库前剔除。模型解析 = 活动覆盖 ?? 全局分析默认,两者皆无 → 明确错误。API:`GET /api/campaigns/{id}/analysis`(未生成返回空态)、`POST /api/campaigns/{id}/analysis`(触发/覆盖重跑;进行中/计划中 400;无模型 400 且 detail 引导配置)。后台 asyncio 任务 + try/finally 关 Session,失败落 error。 + +**Blocked by:** 02(模型解析依赖活动覆盖列) + +**Status:** ready-for-agent + +- [ ] Alembic 迁移:`campaign_analyses` 表(campaign_id 唯一) +- [ ] 仓储层 upsert / get_by_campaign +- [ ] `evaluation/analysis.py` 两阶段编排(阶段一 `asyncio.gather` 并行;输出 schema 见 spec) +- [ ] 失败样例选取:每场景 ≤3 条失败 turn(用户消息/回复/判定理由,截断) +- [ ] run_id 白名单:模型引用的虚构 id 落库前剔除 +- [ ] 假 LLM 客户端单测:编排、解析失败→failed、白名单剔除、无失败样例场景仍产叙述、无模型→明确错误 +- [ ] API 集成测试:GET 空态;POST→完成→GET 拿结果;进行中 400;无模型 400;重跑 upsert 不新增行 +- [ ] `pytest` + `ruff` 全绿 diff --git a/.scratch/v0.7/issues/04-report-drawer-analysis-section.md b/.scratch/v0.7/issues/04-report-drawer-analysis-section.md new file mode 100644 index 0000000..1c92d32 --- /dev/null +++ b/.scratch/v0.7/issues/04-report-drawer-analysis-section.md @@ -0,0 +1,14 @@ +# 04 — 报告抽屉「智能分析」区块 + +**What to build:** 活动报告抽屉顶部新增「智能分析」区块。状态行:生成中(spinner + 5s 轮询直到终态)、失败(错误原因 + 重试按钮)、未生成(「生成分析」按钮;无分析模型时引导去配置中心)。已生成时依次渲染:总体结论 callout → 问题诊断列表(严重度 Tag 高/中/低配色、涉及场景名、证据 chip 点击跳 `/reports?run={id}`)→ 分场景叙述 → 改善建议(按 priority 排序)。生成/重新生成按钮仅终态活动可用,进行中/计划中禁用并提示「活动完成后可生成」。显示所用分析模型名与生成时间。 + +**Blocked by:** 03(API 与结果结构) + +**Status:** ready-for-agent + +- [ ] `api.ts`:`CampaignAnalysis` 类型 + `campaignsApi.getAnalysis(id)` / `generateAnalysis(id)` +- [ ] 抽屉顶部「智能分析」区块按状态渲染(生成中轮询复用 `usePolling`) +- [ ] 四个结构化区块渲染:总体结论 / 问题诊断(严重度配色 + 场景名 + 证据 chip 跳转)/ 分场景叙述 / 改善建议排序 +- [ ] 按钮状态:终态可点;进行中/计划中禁用 + 提示;无分析模型引导配置中心 +- [ ] 展示分析模型名 + 生成时间 +- [ ] `npx tsc --noEmit` + `npm run build` 通过;浏览器实操验证全流程 diff --git a/.scratch/v0.7/issues/05-auto-trigger-and-markdown-export.md b/.scratch/v0.7/issues/05-auto-trigger-and-markdown-export.md new file mode 100644 index 0000000..4eeb33d --- /dev/null +++ b/.scratch/v0.7/issues/05-auto-trigger-and-markdown-export.md @@ -0,0 +1,14 @@ +# 05 — 正式线自动触发 + Markdown 导出纳入 + +**What to build:** 正式线活动(`time_scale == 1`)在 runner 写入 COMPLETED 的同一处自动 enqueue 分析后台任务(`triggered_by=auto`);加速活动(`time_scale > 1`)与失败/取消的活动不自动触发;无可解析分析模型时静默跳过(不阻塞活动完成)。活动 Markdown 导出(`render_campaign_markdown`)在该活动存在 completed 分析时追加分析区块:总体结论 / 问题诊断(含严重度、场景、证据 run 引用)/ 分场景叙述 / 改善建议。 + +**Blocked by:** 03(分析服务与存储) + +**Status:** ready-for-agent + +- [ ] runner 完成处钩子:`time_scale == 1` 且有可解析分析模型 → enqueue(用假分析服务断言调用/不调用两侧) +- [ ] 加速线、failed、cancelled 活动不自动触发 +- [ ] 无分析模型时静默跳过,活动完成流程不受影响 +- [ ] `render_campaign_markdown` 追加分析区块(仅 completed 分析);无分析时导出与现状一致 +- [ ] 测试:自动触发三分支 + Markdown 含/不含分析区块两侧 +- [ ] `pytest` + `ruff` 全绿 diff --git a/.scratch/v0.7/spec.md b/.scratch/v0.7/spec.md new file mode 100644 index 0000000..bbccc28 --- /dev/null +++ b/.scratch/v0.7/spec.md @@ -0,0 +1,87 @@ +# v0.7 — 活动智能分析(分析 Agent + 结构化分析报告) + +## Problem Statement + +评测工程师拿到活动报告时,看到的是一堆聚合数字(通过率、可用性、时延、趋势、分场景汇总),但「这个数字意味着什么、哪里出了问题、接下来该改什么」需要人自己钻进几十条子运行里翻失败对话,耗时且依赖经验。尤其是正式线(实时 ×1)活动跑完后,团队期望直接拿到一份可读的专业研判:总体结论、问题清单、改进建议——目前平台给不出。 + +## Solution + +为活动引入「分析(Analysis)岗位」(CONTEXT.md 已有术语):一个两阶段分析 Agent,对活动聚合结果做活动级、跨场景的叙述性研判,产出**结构化分析报告**(总体结论 + 问题诊断 + 分场景叙述 + 改善建议),嵌在活动报告抽屉顶部展示,证据可点击下钻到具体子运行。正式线活动完成后自动生成;调试线(加速)活动按需手动生成,结果缓存可重新生成。分析模型在模型配置中心设「分析默认」,活动创建时可覆盖。 + +## User Stories + +1. 作为评测工程师,我希望正式线活动一完成就自动得到分析报告,以便不用记得去点按钮。 +2. 作为评测工程师,我希望调试线活动不自动消耗分析模型的 token,以便控制成本。 +3. 作为评测工程师,我希望在报告抽屉里对终态活动随时手动生成/重新生成分析,以便在调整数据视角后拿到新研判。 +4. 作为评测工程师,我希望进行中的活动不能生成分析,以便报告永远基于稳定数据。 +5. 作为评测工程师,我希望报告开头有一段总体结论,以便 30 秒内知道这个活动行不行。 +6. 作为评测工程师,我希望看到按严重度排列的问题诊断列表,每条注明涉及场景和证据,以便优先处理大问题。 +7. 作为评测工程师,我希望问题证据能直接跳到对应子运行报告,以便核实模型研判是否属实。 +8. 作为评测工程师,我希望每个场景有一段叙述性表现分析,以便理解数字背后的行为模式。 +9. 作为评测工程师,我希望改善建议按优先级排序且可执行,以便直接转成下一步工作。 +10. 作为评测工程师,我希望模型引用的证据都真实存在(不虚构 run_id),以便信任报告。 +11. 作为管理员,我希望在模型配置中心把某个 chat 模型设为「分析默认」,以便全平台统一分析口径。 +12. 作为管理员,我希望「分析默认」全局唯一、设置时自动互斥,以便不用手动清理旧标记。 +13. 作为评测工程师,我希望创建活动时能为该活动单独指定分析模型,以便重要活动用更强的模型。 +14. 作为评测工程师,我希望未配置分析模型时得到明确引导(去配置中心设置),以便知道为什么没有分析。 +15. 作为评测工程师,我希望分析失败时看到失败原因并能重试,以便区分是模型故障还是数据问题。 +16. 作为评测工程师,我希望导出的活动 Markdown 报告包含分析区块,以便离线分享完整报告。 +17. 作为评测工程师,我希望分析所用的模型配置被快照记录,以便事后追溯报告出自哪个模型。 +18. 作为评测工程师,我希望报告抽屉里能区分「生成中 / 已完成 / 失败」状态,以便知道后台任务进展。 + +## Implementation Decisions + +- **分析岗位**:`ModelPurpose` 枚举新增 `ANALYSIS`。分析模型不按场景绑定(区别于出题/判卷),而是全局默认 + 活动覆盖(CONTEXT.md 术语定义)。 +- **分析默认标记**:`model_configs` 表新增 `is_analysis_default` 布尔列(Alembic 迁移,batch mode,server_default 回填 false)。仓储层参照 `is_default` 的 `clear_default` 模式保证全局唯一;仅启用的 chat 能力配置可设为分析默认(校验与 `is_default and not enabled` 同款)。模型配置中心 UI 在 chat 配置上提供「分析默认」标记。 +- **活动覆盖**:`campaigns` 表新增可空 `analysis_model_config_id` 列(迁移);创建活动 API 接受该字段(可空=跟随全局默认);创建表单左列「时间与速度」区加「分析模型」下拉(选项=启用的 chat 配置,默认项「全局默认(<默认配置名>)」)。 +- **分析模型解析**:`活动.analysis_model_config_id ?? 全局分析默认`。两者都无 → 自动触发跳过;手动触发返回 409/400 并提示去配置中心。 +- **分析存储**:新表 `campaign_analyses`,每活动一行(upsert 覆盖):`campaign_id` 唯一、`status`(generating/completed/failed)、`result`(结构化 JSON,可空)、`model_config_id`(快照)、`error`(可空)、`triggered_by`(auto/manual)、`created_at`/`updated_at`。后台任务与 Runs 同款 `asyncio.Task` + `try/finally` 关 Session。 +- **API**:`GET /api/campaigns/{id}/analysis` → `{status, result?, error?, model_config_id?, updated_at?}`(从未生成时 status 为 none/空态);`POST /api/campaigns/{id}/analysis` → 触发生成(已存在则覆盖重跑);活动为进行中/计划中返回 400;无分析模型可解析返回 400 且 detail 引导配置。 +- **自动触发**:`campaign_runner` 写入 COMPLETED 的同一处(campaign_runner.py:232 附近),若 `time_scale == 1` 且可解析到分析模型,则 enqueue 分析后台任务。失败/取消的活动不自动触发。 +- **两阶段分析 Agent**(新模块 `evaluation/analysis.py`): + - 输入数据:复用 `generate_campaign_report` 的聚合结果(不重算,遵循 ADR-0002/0004 口径)+ 每场景最多 3 条代表性失败对话(取自失败子运行的 turn:用户消息/回复/判定理由,各截断到合理长度)。 + - 阶段一:每个场景一次 LLM 调用(并行 `asyncio.gather`)→ 该场景叙述 + 问题点草稿。 + - 阶段二:汇总各场景产出 + 全局统计 → 总体结论 + 跨场景问题 + 优先级建议。 + - 输出用 JSON schema 约束(prompt 内嵌 schema + 解析校验,解析失败按失败处理可重试)。 + - 证据白名单:`run_id` 必须在提供的数据集内,模型虚构的引用在落库前剔除。 +- **结构化报告 schema**: + ``` + { + "overall": str, # 总体结论(一段话) + "problems": [{"severity": "high"|"medium"|"low", + "title": str, "description": str, + "scenario_ids": [str], + "evidence_run_ids": [str]}], + "scenario_narratives": [{"scenario_id": str, "narrative": str}], + "suggestions": [{"priority": int, "text": str}] + } + ``` +- **前端报告抽屉**:顶部新增「智能分析」区块——状态行(生成中 spinner / 失败 error+重试 / 未生成时的生成按钮与状态说明 + 分析模型名);完成后渲染:总体结论 callout → 问题诊断列表(严重度 Tag、场景名、证据 chip 点击跳 `/reports?run=`)→ 分场景叙述 → 改善建议(按 priority 排序)。生成/重新生成按钮仅终态活动可用,进行中禁用并提示「活动完成后可生成」。 +- **Markdown 导出**:`render_campaign_markdown` 追加分析区块(有 completed 分析时),按 总体结论/问题诊断/分场景叙述/改善建议 拼段。 +- **语言**:分析输出一律中文。 + +## Testing Decisions + +好测试只测外部行为,不测实现细节。 + +- **唯一新 seam:分析服务**(`evaluation/analysis.py`),用假 LLM 客户端注入(先例:`tests/unit/mock_channel.py` 的 MockChannel、llm_score 的假网关)。覆盖:两阶段编排(阶段一并行、阶段二汇总)、JSON 解析失败→ failed 状态、run_id 白名单剔除虚构引用、无失败样例的场景也能产出叙述、无分析模型可解析→明确错误。 +- **API 集成测试**(先例 `tests/integration/test_campaigns_api.py`):GET 空态;POST 触发→后台完成→GET 拿到结果;进行中活动 POST 400;无分析模型 POST 400;覆盖重跑 upsert 不新增行。 +- **自动触发**:runner 完成正式线活动后分析任务被 enqueue(用假分析服务断言调用);加速活动不触发。 +- **分析默认唯一性**:仓储/服务层测试(设第二个分析默认时第一个被清;停用配置不能设为分析默认)——先例 `is_default` 现有测试。 +- **迁移**:Alembic upgrade 后两表新列存在且回填正确。 +- **前端**:无测试框架,`npx tsc --noEmit` + `npm run build` + 浏览器实操验证(生成流程、状态展示、证据跳转、终态禁用)。 + +## Out of Scope + +- **工具调用循环 Agent**(function calling 让模型自主拉数据)——本批固定两阶段编排。 +- **HTML/PDF 专业报告页**——本批结构化 JSON + 抽屉原生渲染 + Markdown 导出。 +- **分析报告的历史版本**——重新生成即覆盖(upsert),不保留旧版。 +- **分析结果的通知推送**(完成提醒、Webhook)。 +- **跨活动对比分析**。 +- **进行中活动的增量分析**。 + +## Further Notes + +- 分析输入完全来自既有聚合口径(ADR-0002:失败按 0.0 计入;ADR-0004:取消不计入分母),分析 Agent 不重算数字,只做叙述性研判。 +- 阶段一按场景并行,场景数 = 该活动 capability_summary 中出现的场景数;阶段二单次调用。 +- 分析模型的上下文窗口/最大输出元数据沿用模型配置中心的描述性元数据,不改变网关请求参数(AGENTS.md 模型配置中心约定)。 diff --git a/CONTEXT.md b/CONTEXT.md index 2041218..53f7d92 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -81,5 +81,9 @@ _Avoid_: 在线率、健康度 _Avoid_: 功能、类型 **模型用途(Purpose)**: -需求侧属性:评测流程中的角色岗位(出题 generator / 判卷 judge / 向量 embedding / 审核 moderation)。场景通过模型绑定为每个岗位指派一个具备相应能力的模型;一个对话能力模型可同时胜任出题与判卷两个岗位。 +需求侧属性:评测流程中的角色岗位(出题 generator / 判卷 judge / 向量 embedding / 审核 moderation / 分析 analysis)。场景通过模型绑定为每个岗位指派一个具备相应能力的模型;一个对话能力模型可同时胜任出题、判卷、分析等岗位。 _Avoid_: 能力、角色(role 留给对话消息的 role 字段) + +**分析(Analysis)岗位**: +模型用途的一种:对一整次评估活动(Campaign)的聚合结果做复杂诊断,产出结构化的问题分析与服务质量改善建议。区别于判卷(judge 只裁单轮回复),分析是活动级、跨场景的叙述性研判。因活动跨场景,分析模型不按场景绑定,而是全局默认指派、活动创建时可覆盖。 +_Avoid_: 判卷、总结 diff --git a/backend/agenteval/models.py b/backend/agenteval/models.py index 8142ddd..3766cf3 100644 --- a/backend/agenteval/models.py +++ b/backend/agenteval/models.py @@ -55,6 +55,7 @@ class ModelPurpose(str, Enum): JUDGE = "judge" EMBEDDING = "embedding" MODERATION = "moderation" + ANALYSIS = "analysis" class EvalTarget(BaseModel): diff --git a/backend/agenteval/services/model_configs.py b/backend/agenteval/services/model_configs.py index d0db5ff..a31c245 100644 --- a/backend/agenteval/services/model_configs.py +++ b/backend/agenteval/services/model_configs.py @@ -146,6 +146,15 @@ class ModelConfigService: if is_default and not enabled: raise ModelConfigError("停用的模型配置不能设为默认") + @staticmethod + def validate_analysis_default(capability: str, enabled: bool, is_analysis_default: bool) -> None: + if not is_analysis_default: + return + if not enabled: + raise ModelConfigError("停用的模型配置不能设为分析默认") + if capability != ModelCapability.CHAT.value: + raise ModelConfigError("分析默认模型必须是 chat 能力") + @staticmethod def normalize_modalities(modalities: list[str] | None) -> list[str]: values = modalities if modalities is not None else [ModelModality.TEXT.value] @@ -199,6 +208,7 @@ class ModelConfigService: supports_reasoning: bool = False, region: str = "", documentation_url: str | None = None, + is_analysis_default: bool = False, ) -> ModelConfigDB: name = name.strip() input_modalities = self.normalize_modalities(input_modalities) @@ -206,6 +216,7 @@ class ModelConfigService: self.validate_common(name, provider, enabled, is_default) self.validate_fields(provider, capability, endpoint_url.strip(), model_name) self.validate_metadata(context_window, max_output_tokens, documentation_url) + self.validate_analysis_default(capability, enabled, is_analysis_default) if self.repo.get_by_name(name): raise ModelConfigError("模型配置名称已存在") config = ModelConfigDB( @@ -226,6 +237,7 @@ class ModelConfigService: api_key_encrypted=self.cipher.encrypt(api_key), enabled=enabled, is_default=is_default, + is_analysis_default=is_analysis_default, description=description.strip(), ) config.set_modalities(input_modalities, output_modalities) @@ -256,6 +268,7 @@ class ModelConfigService: supports_reasoning: bool = False, region: str = "", documentation_url: str | None = None, + is_analysis_default: bool = False, ) -> ModelConfigDB: config = self.require(config_id) name = name.strip() @@ -264,6 +277,7 @@ class ModelConfigService: self.validate_common(name, provider, enabled, is_default) self.validate_fields(provider, capability, endpoint_url.strip(), model_name) self.validate_metadata(context_window, max_output_tokens, documentation_url) + self.validate_analysis_default(capability, enabled, is_analysis_default) references = self.repo.list_references(config_id) if references and not enabled: raise ModelConfigError("模型配置正在被场景引用,不能停用") @@ -293,6 +307,7 @@ class ModelConfigService: config.documentation_url = documentation_url.strip() if documentation_url else None config.enabled = enabled config.is_default = is_default + config.is_analysis_default = is_analysis_default config.description = description.strip() if clear_api_key: config.api_key_encrypted = None diff --git a/backend/agenteval/storage/db.py b/backend/agenteval/storage/db.py index 2090bd4..c36b31f 100644 --- a/backend/agenteval/storage/db.py +++ b/backend/agenteval/storage/db.py @@ -145,6 +145,7 @@ class ModelConfigDB(SQLModel, table=True): api_key_encrypted: Optional[str] = None enabled: bool = True is_default: bool = False + is_analysis_default: bool = False description: str = "" created_at: Optional[datetime] = Field(default_factory=utc_now) updated_at: Optional[datetime] = Field(default_factory=utc_now) diff --git a/backend/agenteval/storage/model_config_repository.py b/backend/agenteval/storage/model_config_repository.py index 7b6eefc..3b0f26d 100644 --- a/backend/agenteval/storage/model_config_repository.py +++ b/backend/agenteval/storage/model_config_repository.py @@ -31,6 +31,8 @@ class ModelConfigRepository: def create(self, config: ModelConfigDB) -> ModelConfigDB: if config.is_default: self.clear_default(config.capability) + if config.is_analysis_default: + self.clear_analysis_default() self.session.add(config) self.session.commit() self.session.refresh(config) @@ -39,6 +41,8 @@ class ModelConfigRepository: def update(self, config: ModelConfigDB) -> ModelConfigDB: if config.is_default: self.clear_default(config.capability, exclude_id=config.id) + if config.is_analysis_default: + self.clear_analysis_default(exclude_id=config.id) config.updated_at = utc_now() self.session.add(config) self.session.commit() @@ -61,6 +65,15 @@ class ModelConfigRepository: item.updated_at = utc_now() self.session.add(item) + def clear_analysis_default(self, exclude_id: str | None = None) -> None: + statement = select(ModelConfigDB).where(ModelConfigDB.is_analysis_default.is_(True)) + for item in self.session.exec(statement).all(): + if item.id == exclude_id: + continue + item.is_analysis_default = False + item.updated_at = utc_now() + self.session.add(item) + def list_references(self, config_id: str) -> list[ScenarioModelBindingDB]: statement = select(ScenarioModelBindingDB).where( ScenarioModelBindingDB.model_config_id == config_id, diff --git a/backend/agenteval/web/model_config_schemas.py b/backend/agenteval/web/model_config_schemas.py index 91f8b9f..835190e 100644 --- a/backend/agenteval/web/model_config_schemas.py +++ b/backend/agenteval/web/model_config_schemas.py @@ -28,6 +28,7 @@ class ModelConfigCreate(BaseModel): api_key: str | None = None enabled: bool = True is_default: bool = False + is_analysis_default: bool = False description: str = "" @@ -56,6 +57,7 @@ class ModelConfigResponse(BaseModel): has_api_key: bool enabled: bool is_default: bool + is_analysis_default: bool description: str created_at: str | None updated_at: str | None @@ -83,6 +85,7 @@ class ModelConfigResponse(BaseModel): has_api_key=bool(config.api_key_encrypted), enabled=config.enabled, is_default=config.is_default, + is_analysis_default=config.is_analysis_default, description=config.description, created_at=iso_utc(config.created_at), updated_at=iso_utc(config.updated_at), diff --git a/frontend/web/src/api.ts b/frontend/web/src/api.ts index b859e93..e24cebe 100644 --- a/frontend/web/src/api.ts +++ b/frontend/web/src/api.ts @@ -100,6 +100,7 @@ export interface ModelConfig { has_api_key: boolean enabled: boolean is_default: boolean + is_analysis_default: boolean description: string created_at: string | null updated_at: string | null @@ -126,6 +127,7 @@ export interface ModelConfigPayload { clear_api_key?: boolean enabled: boolean is_default: boolean + is_analysis_default?: boolean description: string } diff --git a/frontend/web/src/pages/ModelConfigs.tsx b/frontend/web/src/pages/ModelConfigs.tsx index 52ff923..e2d0cbd 100644 --- a/frontend/web/src/pages/ModelConfigs.tsx +++ b/frontend/web/src/pages/ModelConfigs.tsx @@ -116,6 +116,7 @@ export default function ModelConfigsPage() { documentation_url: null, enabled: true, is_default: false, + is_analysis_default: false, description: '', }) setDrawerOpen(true) @@ -144,6 +145,7 @@ export default function ModelConfigsPage() { clear_api_key: false, enabled: config.enabled, is_default: config.is_default, + is_analysis_default: config.is_analysis_default, description: config.description, }) setDrawerOpen(true) @@ -208,6 +210,7 @@ export default function ModelConfigsPage() { {value} {record.is_default && 默认} + {record.is_analysis_default && 分析默认}