From 8461d5ce4dac8520b5b2ecfd6651efcae0e9dfb8 Mon Sep 17 00:00:00 2001 From: wizardchen Date: Tue, 21 Apr 2026 00:37:20 +0800 Subject: [PATCH] feat: Enhance agent system prompt and localization for wiki tools - Updated the agent system prompt to include new auxiliary tools and improved synthesis and issue flagging steps, enhancing the clarity of the workflow. - Revised constraints to emphasize the importance of retrieving information from the wiki first and using skills when applicable. - Expanded localization files for English, Korean, Russian, and Chinese to include new wiki tool descriptions and statuses, ensuring consistency across languages. - Improved the agent editor modal to provide a clearer overview of tool statuses and retrieval preferences, enhancing user experience. These changes significantly improve the functionality and usability of the agent system, particularly in relation to wiki interactions and tool management. --- .../prompt_templates/agent_system_prompt.yaml | 20 +- frontend/src/i18n/locales/en-US.ts | 51 ++ frontend/src/i18n/locales/ko-KR.ts | 51 ++ frontend/src/i18n/locales/ru-RU.ts | 53 +- frontend/src/i18n/locales/zh-CN.ts | 51 ++ frontend/src/views/agent/AgentEditorModal.vue | 679 ++++++++++++++++-- internal/application/service/agent_service.go | 275 ++++--- 7 files changed, 1021 insertions(+), 159 deletions(-) diff --git a/config/prompt_templates/agent_system_prompt.yaml b/config/prompt_templates/agent_system_prompt.yaml index 794b004f6..0113b9017 100644 --- a/config/prompt_templates/agent_system_prompt.yaml +++ b/config/prompt_templates/agent_system_prompt.yaml @@ -254,16 +254,22 @@ templates: - **Incoming links (`Linked from`):** Use these to find broader context, discover what other projects/entities reference this topic, or see where this concept is applied. If the current page doesn't fully answer the question, you MUST call `wiki_read_page` again on these related slugs (1-2 hops). 4. **Drill-down Fallback (Optional):** Wiki pages summarize information from source documents. If the user asks for highly specific details (e.g. exact quotes, raw data, or specific code snippets) that are not present in the wiki page, check the `` section of that wiki page. Use the `wiki_read_source_doc` tool with the provided `knowledge_id` to read the original raw document, optionally using a regex query to find the specific detail. - 5. **Synthesize:** Once you have gathered sufficient information from reading multiple interconnected pages (and optionally source documents), synthesize a final answer. If you encountered images (e.g. ``) in the source documents that are relevant to the user's query, be sure to output them in your final response using Markdown format (`![alt](url)`). - 6. **Flag Issues (If Necessary):** If you discover that a wiki page contains factual errors, mixed entities (e.g., two different products combined into one page), or outdated information, OR if the user points out such errors, use the `wiki_flag_issue` tool to submit a maintenance report for that page. + 5. **Auxiliary Tools (Optional):** Beyond the wiki itself, you may leverage auxiliary capabilities when they clearly help answer the question: + - **External MCP Tools (if any are exposed in your tool list):** Use them when the user's question requires real-time data, external system lookups, or actions that do NOT live inside the wiki (e.g. querying a ticketing system, calling an internal API, fetching live metrics). Treat their responses as additional evidence, not as a replacement for wiki content. + - **Skills (if an `### Available Skills` section appears later in this prompt):** Before answering, scan the listed skills. If the user's intent matches a skill's triggers, call `read_skill(skill_name="...")` to load its full instructions, then follow them. Skills are especially useful for specialized output formatting or domain-specific procedures. + Wiki content remains the primary source of truth for domain facts; MCP tools and skills are complementary. + 6. **Synthesize:** Once you have gathered sufficient information from reading multiple interconnected pages (and optionally source documents, MCP results, or skill guidance), synthesize a final answer. If you encountered images (e.g. ``) in the source documents that are relevant to the user's query, be sure to output them in your final response using Markdown format (`![alt](url)`). + 7. **Flag Issues (If Necessary):** If you discover that a wiki page contains factual errors, mixed entities (e.g., two different products combined into one page), or outdated information, OR if the user points out such errors, use the `wiki_flag_issue` tool to submit a maintenance report for that page. ABSOLUTE RULES: - 1. **Never Guess:** Never rely on your internal knowledge. Only answer based on what you have successfully read via `wiki_read_page`. - 2. **Mandatory Reading:** `wiki_search` only returns summaries. You CANNOT write a final answer based solely on `wiki_search` results. You MUST call `wiki_read_page` on the relevant slugs to get the actual facts. - 3. **Cite Sources:** Your final answer must clearly state which wiki pages you derived the information from, and MUST include wiki-links to them in the format `[[slug|display name]]` so the user can click them to navigate. - 4. **Always Re-Retrieve:** For every new question, you MUST perform fresh searches and reads. Do not rely on your memory of previous turns, as the wiki content may have changed. + 1. **Never Guess:** Never rely on your internal parametric knowledge. Ground every factual claim in evidence you have actually retrieved in this turn — primarily from `wiki_read_page` / `wiki_read_source_doc`, and secondarily from verified MCP tool responses when the information legitimately lives outside the wiki. + 2. **Wiki First:** For any domain fact that could plausibly be covered by the wiki, you MUST search and read the wiki before falling back to MCP tools or general reasoning. Do not skip wiki retrieval just because an MCP tool looks convenient. + 3. **Mandatory Reading:** `wiki_search` only returns summaries. You CANNOT write a final answer based solely on `wiki_search` results. You MUST call `wiki_read_page` on the relevant slugs to get the actual facts. + 4. **Cite Sources:** Your final answer must clearly state which wiki pages you derived the information from, and MUST include wiki-links to them in the format `[[slug|display name]]` so the user can click them to navigate. For facts obtained from MCP tools or source documents, briefly attribute them inline (e.g. "根据 Jira 工单 ABC-123 …"). + 5. **Always Re-Retrieve:** For every new question, you MUST perform fresh searches and reads. Do not rely on your memory of previous turns, as the wiki content may have changed. + 6. **Use Skills When They Apply:** If the `### Available Skills` section is present and any listed skill clearly matches the task (by keyword, scenario, or task type), you MUST call `read_skill` to load it before producing the final answer. @@ -271,6 +277,8 @@ templates: * **wiki_read_page:** Your primary tool. Use it to read the full content of pages found via search, linked from other pages, or the special `index` and `log` pages. You can provide multiple slugs at once to read multiple pages in parallel. * **wiki_read_source_doc:** Use this ONLY as a fallback when a wiki page's content is insufficient and you need to dive into the raw source document listed in the page's `` section. Use the `knowledge_id` and an optional regex `query` to efficiently find specific quotes or details, OR use `start_chunk_index` and `end_chunk_index` to fetch a contiguous range of chunks. * **wiki_flag_issue:** Use this tool when you or the user identifies that a wiki page is factually incorrect, contains outdated information, or wrongly merges distinct entities (e.g., merging a competitor's product into the current page). This logs an issue for human review or automated maintenance. Provide a clear description and any suspected source `knowledge_id`s that might be causing the problem. + * **MCP Tools (dynamic):** Any tools whose names do NOT start with `wiki_` and are not the built-in `thinking` / `todo_write` are external MCP tools exposed by connected services. Use them when the task needs real-time or external data that the wiki cannot provide. Their results are evidence — cite them inline and never let them override facts already confirmed from the wiki. + * **read_skill / execute_skill_script (only if skills are exposed):** Load and run skills listed under `### Available Skills`. `read_skill` fetches full skill instructions; `execute_skill_script` runs a script shipped with a skill (only available when the sandbox is enabled). diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index 7b42571c3..b781eeb9e 100755 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -3532,6 +3532,57 @@ export default { dataSchema: 'View Data Schema', dataSchemaDesc: 'Get metadata of tabular files', requiresKb: '(requires knowledge base configuration)', + requiresRagKb: '(requires a KB with vector/keyword indexing enabled)', + requiresWikiKb: '(requires a Wiki-enabled knowledge base)', + // Wiki read tools + wikiSearch: 'Search Wiki', + wikiSearchDesc: 'Keyword / semantic search over Wiki pages', + wikiReadPage: 'Read Wiki Page', + wikiReadPageDesc: 'Read the full content of a specific Wiki page', + wikiReadSourceDoc: 'Deep-read Source Doc', + wikiReadSourceDocDesc: 'Dive into the original documents that a Wiki page is built from', + wikiFlagIssue: 'Flag Wiki Issue', + wikiFlagIssueDesc: 'Flag factual errors or merge conflicts on a Wiki page', + // Wiki write tools (destructive) + wikiWritePage: 'Create / Overwrite Wiki', + wikiWritePageDesc: 'Create a new page or fully overwrite an existing one', + wikiReplaceText: 'Replace Text in Wiki', + wikiReplaceTextDesc: 'Replace specific text in a Wiki page', + wikiRenamePage: 'Rename Wiki Page', + wikiRenamePageDesc: 'Rename a Wiki page and auto-update cross-links', + wikiDeletePage: 'Delete Wiki Page', + wikiDeletePageDesc: 'Delete a Wiki page and clean up dead links', + // Wiki issue tools + wikiReadIssue: 'View Wiki Issue', + wikiReadIssueDesc: 'View details of a Wiki page issue', + wikiUpdateIssue: 'Update Wiki Issue', + wikiUpdateIssueDesc: 'Update the status of a Wiki page issue', + // Runtime system-injected tools (preview only) + webSearch: 'Web Search', + webFetch: 'Web Fetch', + finalAnswer: 'Submit Final Answer', + // Groups + groupBase: 'Basic', + groupRag: 'Knowledge Retrieval (RAG)', + groupWikiRead: 'Wiki Read', + groupWikiEdit: 'Wiki Edit', + groupWikiIssue: 'Wiki Review', + groupData: 'Data Analysis', + writeWarning: 'Mutates Wiki content', + dangerTag: 'Write', + // Preset + presetLabel: 'Retrieval Preset', + presetDesc: 'Switching the preset will update the checkboxes below (RAG / Wiki tools) in one click', + // Status bar + statusNoKb: 'No knowledge base is linked', + statusKb: 'In scope: {rag} RAG KBs · {wiki} Wiki KBs', + kbMetricRag: 'RAG KBs', + kbMetricWiki: 'Wiki KBs', + statusInactive: '{count} ticked tool(s) cannot take effect with the current config', + // Effective tools preview + effectiveLabel: 'Effective Tools', + effectiveDesc: 'Computed from the current config — these are the tools the agent will actually be able to call', + effectiveEmpty: 'No tool available — the agent will fall back to plain model chat', }, im: { title: 'IM Integration', diff --git a/frontend/src/i18n/locales/ko-KR.ts b/frontend/src/i18n/locales/ko-KR.ts index 7bb229402..1e1667020 100755 --- a/frontend/src/i18n/locales/ko-KR.ts +++ b/frontend/src/i18n/locales/ko-KR.ts @@ -3584,6 +3584,57 @@ export default { dataSchema: '데이터 스키마 보기', dataSchemaDesc: '테이블 파일의 메타 정보 조회', requiresKb: '(지식베이스 설정 필요)', + requiresRagKb: '(벡터/키워드 인덱싱이 활성화된 지식베이스가 필요합니다)', + requiresWikiKb: '(Wiki 기능이 활성화된 지식베이스가 필요합니다)', + // Wiki 읽기 + wikiSearch: 'Wiki 검색', + wikiSearchDesc: 'Wiki 지식베이스에서 키워드/의미 기반 검색', + wikiReadPage: 'Wiki 페이지 읽기', + wikiReadPageDesc: '지정한 Wiki 페이지의 전체 내용을 읽습니다', + wikiReadSourceDoc: '원본 문서 읽기', + wikiReadSourceDocDesc: 'Wiki 페이지의 기반이 된 원본 문서를 깊이 있게 읽습니다', + wikiFlagIssue: 'Wiki 문제 표시', + wikiFlagIssueDesc: '사실 오류나 병합 충돌을 표시합니다', + // Wiki 편집 (파괴적) + wikiWritePage: 'Wiki 생성/덮어쓰기', + wikiWritePageDesc: '새 페이지 생성 또는 기존 페이지 완전 덮어쓰기', + wikiReplaceText: 'Wiki 텍스트 교체', + wikiReplaceTextDesc: 'Wiki 페이지의 특정 텍스트 교체', + wikiRenamePage: 'Wiki 이름 변경', + wikiRenamePageDesc: 'Wiki 페이지 이름을 변경하고 연결된 링크를 자동 갱신', + wikiDeletePage: 'Wiki 삭제', + wikiDeletePageDesc: 'Wiki 페이지를 삭제하고 끊어진 링크를 자동 정리', + // Wiki 점검 + wikiReadIssue: 'Wiki 문제 보기', + wikiReadIssueDesc: '특정 Wiki 페이지 문제의 상세 조회', + wikiUpdateIssue: 'Wiki 문제 업데이트', + wikiUpdateIssueDesc: 'Wiki 페이지 문제의 처리 상태 업데이트', + // 런타임 주입 (미리보기용) + webSearch: '웹 검색', + webFetch: '웹 페이지 가져오기', + finalAnswer: '최종 답변 제출', + // 그룹 + groupBase: '기본', + groupRag: '지식베이스 검색 (RAG)', + groupWikiRead: 'Wiki 읽기', + groupWikiEdit: 'Wiki 편집', + groupWikiIssue: 'Wiki 점검', + groupData: '데이터 분석', + writeWarning: 'Wiki 내용을 변경합니다', + dangerTag: '쓰기', + // 프리셋 + presetLabel: '검색 전략 프리셋', + presetDesc: '프리셋 전환 시 아래 체크박스(RAG/Wiki 도구)가 일괄 조정됩니다', + // 상태바 + statusNoKb: '연결된 지식베이스가 없습니다', + statusKb: '범위 내: RAG 지식베이스 {rag}개 · Wiki 지식베이스 {wiki}개', + kbMetricRag: 'RAG 지식베이스', + kbMetricWiki: 'Wiki 지식베이스', + statusInactive: '{count}개의 체크된 도구가 현재 설정에서는 작동하지 않습니다', + // 실제 활성 도구 미리보기 + effectiveLabel: '최종 활성 도구', + effectiveDesc: '현재 설정 기준 저장 시 에이전트가 실제 호출할 수 있는 도구 집합', + effectiveEmpty: '사용 가능한 도구가 없어 일반 모델 대화로 동작합니다', }, mcp: { label: 'MCP 서비스', diff --git a/frontend/src/i18n/locales/ru-RU.ts b/frontend/src/i18n/locales/ru-RU.ts index 9fdd3271a..a5c24e419 100755 --- a/frontend/src/i18n/locales/ru-RU.ts +++ b/frontend/src/i18n/locales/ru-RU.ts @@ -3153,7 +3153,58 @@ export default { dataAnalysisDesc: 'Понимание файлов данных и проведение анализа', dataSchema: 'Схема данных', dataSchemaDesc: 'Получение метаинформации табличных файлов', - requiresKb: '(требуется настройка базы знаний)' + requiresKb: '(требуется настройка базы знаний)', + requiresRagKb: '(требуется база знаний с включённым векторным/ключевым индексом)', + requiresWikiKb: '(требуется база знаний с включённой Wiki)', + // Wiki чтение + wikiSearch: 'Поиск по Wiki', + wikiSearchDesc: 'Поиск страниц Wiki по ключевым словам / семантике', + wikiReadPage: 'Читать страницу Wiki', + wikiReadPageDesc: 'Прочитать полное содержимое указанной Wiki-страницы', + wikiReadSourceDoc: 'Читать исходный документ', + wikiReadSourceDocDesc: 'Глубокое чтение исходных документов, на которых построена Wiki-страница', + wikiFlagIssue: 'Отметить проблему в Wiki', + wikiFlagIssueDesc: 'Отметить фактические ошибки или конфликты слияния', + // Wiki редактирование (разрушительное) + wikiWritePage: 'Создать/перезаписать Wiki', + wikiWritePageDesc: 'Создать новую или полностью перезаписать существующую страницу', + wikiReplaceText: 'Заменить текст в Wiki', + wikiReplaceTextDesc: 'Заменить конкретный текст на Wiki-странице', + wikiRenamePage: 'Переименовать Wiki', + wikiRenamePageDesc: 'Переименовать страницу Wiki и автоматически обновить перекрёстные ссылки', + wikiDeletePage: 'Удалить Wiki', + wikiDeletePageDesc: 'Удалить страницу Wiki и автоматически очистить битые ссылки', + // Wiki проверки + wikiReadIssue: 'Просмотр проблемы Wiki', + wikiReadIssueDesc: 'Просмотр деталей конкретной проблемы страницы Wiki', + wikiUpdateIssue: 'Обновить проблему Wiki', + wikiUpdateIssueDesc: 'Обновить статус проблемы страницы Wiki', + // Служебные (только предпросмотр) + webSearch: 'Поиск в сети', + webFetch: 'Загрузка веб-страницы', + finalAnswer: 'Отправить окончательный ответ', + // Группы + groupBase: 'Базовые', + groupRag: 'Поиск в базе знаний (RAG)', + groupWikiRead: 'Чтение Wiki', + groupWikiEdit: 'Редактирование Wiki', + groupWikiIssue: 'Проверка Wiki', + groupData: 'Анализ данных', + writeWarning: 'Изменяет содержимое Wiki', + dangerTag: 'Запись', + // Пресет + presetLabel: 'Пресет поиска', + presetDesc: 'Переключение пресета одним кликом меняет флажки ниже (инструменты RAG/Wiki)', + // Строка состояния + statusNoKb: 'База знаний не подключена', + statusKb: 'В области: {rag} RAG баз · {wiki} Wiki баз', + kbMetricRag: 'RAG баз', + kbMetricWiki: 'Wiki баз', + statusInactive: '{count} отмеченных инструментов не смогут работать при текущей конфигурации', + // Предпросмотр итоговых инструментов + effectiveLabel: 'Итоговые активные инструменты', + effectiveDesc: 'Рассчитано на основе текущей конфигурации — это набор инструментов, которые агент сможет вызывать после сохранения', + effectiveEmpty: 'Нет доступных инструментов — агент перейдёт к простому чату с моделью' }, im: { title: 'Интеграция IM', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 588d420b5..dd3151c6a 100755 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -3584,6 +3584,57 @@ export default { dataSchema: "查看数据元信息", dataSchemaDesc: "获取表格文件的元信息", requiresKb: "(需要配置知识库)", + requiresRagKb: "(需要关联启用向量/关键词索引的知识库)", + requiresWikiKb: "(需要关联启用 Wiki 能力的知识库)", + // Wiki 读取类 + wikiSearch: "搜索 Wiki", + wikiSearchDesc: "在 Wiki 知识库中按关键词/语义搜索页面", + wikiReadPage: "阅读 Wiki 页面", + wikiReadPageDesc: "读取指定 Wiki 页面的完整内容", + wikiReadSourceDoc: "精读源文档", + wikiReadSourceDocDesc: "通过知识点深入阅读 Wiki 页面背后的原始文档", + wikiFlagIssue: "标记 Wiki 问题", + wikiFlagIssueDesc: "标记页面中存在的事实错误或合并冲突问题", + // Wiki 编辑类(具有破坏性) + wikiWritePage: "创建/覆盖 Wiki", + wikiWritePageDesc: "创建新页面或完全覆盖已有 Wiki 页面", + wikiReplaceText: "局部替换 Wiki", + wikiReplaceTextDesc: "替换 Wiki 页面中的特定文本", + wikiRenamePage: "重命名 Wiki", + wikiRenamePageDesc: "重命名 Wiki 页面并自动更新关联链接", + wikiDeletePage: "删除 Wiki", + wikiDeletePageDesc: "删除 Wiki 页面并自动清理关联死链", + // Wiki 巡检类 + wikiReadIssue: "查看 Wiki 问题", + wikiReadIssueDesc: "查看特定 Wiki 页面的巡检问题详情", + wikiUpdateIssue: "更新 Wiki 问题", + wikiUpdateIssueDesc: "更新 Wiki 页面问题的处理状态", + // 运行时系统注入(只读,用于预览) + webSearch: "网络搜索", + webFetch: "网页抓取", + finalAnswer: "提交最终回答", + // 分组 + groupBase: "基础", + groupRag: "知识库检索(RAG)", + groupWikiRead: "Wiki 读取", + groupWikiEdit: "Wiki 编辑", + groupWikiIssue: "Wiki 巡检", + groupData: "数据分析", + writeWarning: "会修改 Wiki 内容", + dangerTag: "可写", + // 预设 + presetLabel: "检索策略预设", + presetDesc: "切换预设会一次性调整下方勾选(对应的 RAG/Wiki 工具)", + // 顶部状态栏 + statusNoKb: "当前未关联任何知识库", + statusKb: "作用域内:{rag} 个 RAG 知识库 · {wiki} 个 Wiki 知识库", + kbMetricRag: "RAG 知识库", + kbMetricWiki: "Wiki 知识库", + statusInactive: "有 {count} 个已勾选工具在当前配置下无法生效", + // 有效工具预览 + effectiveLabel: "最终启用的工具", + effectiveDesc: "根据当前配置计算,这是智能体保存后实际能调用的工具集合", + effectiveEmpty: "当前没有可用工具,智能体将退化为纯模型问答", }, mcp: { label: "MCP 服务", diff --git a/frontend/src/views/agent/AgentEditorModal.vue b/frontend/src/views/agent/AgentEditorModal.vue index fec353f4b..bbf3409aa 100644 --- a/frontend/src/views/agent/AgentEditorModal.vue +++ b/frontend/src/views/agent/AgentEditorModal.vue @@ -603,33 +603,127 @@

{{ $t('agent.editor.toolsConfig') }}

{{ $t('agent.editor.toolsConfigDesc') }}

- + + +
+
+
+ + + +
+
+ + {{ $t('agentEditor.tools.statusInactive', { count: inactiveToolCount }) }} +
+
+
+
+ {{ $t('agentEditor.tools.presetLabel') }} + {{ $t('agentEditor.tools.presetDesc') }} +
+ + {{ $t('agent.editor.retrievalPreferenceAuto') }} + {{ $t('agent.editor.retrievalPreferenceVectorOnly') }} + {{ $t('agent.editor.retrievalPreferenceWikiOnly') }} + {{ $t('agent.editor.retrievalPreferenceHybrid') }} + +
+
+
- +

{{ $t('agentEditor.desc.selectTools') }}

- - +
-
- {{ tool.label }} - {{ tool.description }} - {{ $t('agentEditor.tools.requiresKb') }} +
+ + {{ group.label }} + {{ group.tools.length }} + + + {{ $t('agentEditor.tools.writeWarning') }} + +
+
+ +
+
+ {{ tool.label }} + + {{ $t('agentEditor.tools.dangerTag') }} + +
+ {{ tool.description }} + + {{ tool.disabledReason }} + +
+
- +
+ +
+
+ +

{{ $t('agentEditor.tools.effectiveDesc') }}

+
+
+
+ + +
+
+
+
@@ -1339,9 +1433,18 @@ const defaultRerankThreshold = ref(0.5); const defaultMaxCompletionTokens = ref(2048); const defaultTemperature = ref(0.7); -// 知识库相关工具列表 +// 知识库相关工具列表(RAG 向量检索工具,依赖 RAG 能力) const knowledgeBaseTools = ['grep_chunks', 'knowledge_search', 'list_knowledge_chunks', 'query_knowledge_graph', 'get_document_info', 'database_query']; +// Wiki 读取类工具(依赖 Wiki 能力,阅读/搜索用途) +const wikiReadTools = ['wiki_search', 'wiki_read_page', 'wiki_read_source_doc', 'wiki_flag_issue']; +// Wiki 编辑类工具(修改 Wiki 内容,需谨慎授权) +const wikiEditTools = ['wiki_write_page', 'wiki_replace_text', 'wiki_rename_page', 'wiki_delete_page']; +// Wiki 巡检类工具 +const wikiIssueTools = ['wiki_read_issue', 'wiki_update_issue']; +// 全部 Wiki 工具 +const allWikiTools = [...wikiReadTools, ...wikiEditTools, ...wikiIssueTools]; + // 初始化标志,防止初始化时触发 watch 自动添加工具 const isInitializing = ref(false); @@ -1355,17 +1458,48 @@ const mcpSelectionMode = ref<'all' | 'selected' | 'none'>('none'); const skillsSelectionMode = ref<'all' | 'selected' | 'none'>('none'); // 可用工具列表 (与后台 definitions.go 保持一致) +// group 决定 UI 分组:base / rag / wiki_read / wiki_edit / wiki_issue / data +// requiresKB: 需要任意知识库配置才可用(配合 hasKnowledgeBase) +// requiresRag: 需要作用域内存在启用了向量/关键词索引的知识库(配合 hasRagKnowledgeBase) +// requiresWiki: 需要作用域内存在启用了 Wiki 索引的知识库(配合 hasWikiKnowledgeBase) +// danger: 写类破坏性工具,UI 上给出显著提示 const allTools = computed(() => [ - { value: 'thinking', label: t('agentEditor.tools.thinking'), description: t('agentEditor.tools.thinkingDesc'), requiresKB: false }, - { value: 'todo_write', label: t('agentEditor.tools.todoWrite'), description: t('agentEditor.tools.todoWriteDesc'), requiresKB: false }, - { value: 'grep_chunks', label: t('agentEditor.tools.grepChunks'), description: t('agentEditor.tools.grepChunksDesc'), requiresKB: true }, - { value: 'knowledge_search', label: t('agentEditor.tools.knowledgeSearch'), description: t('agentEditor.tools.knowledgeSearchDesc'), requiresKB: true }, - { value: 'list_knowledge_chunks', label: t('agentEditor.tools.listChunks'), description: t('agentEditor.tools.listChunksDesc'), requiresKB: true }, - { value: 'query_knowledge_graph', label: t('agentEditor.tools.queryGraph'), description: t('agentEditor.tools.queryGraphDesc'), requiresKB: true }, - { value: 'get_document_info', label: t('agentEditor.tools.getDocInfo'), description: t('agentEditor.tools.getDocInfoDesc'), requiresKB: true }, - { value: 'database_query', label: t('agentEditor.tools.dbQuery'), description: t('agentEditor.tools.dbQueryDesc'), requiresKB: true }, - { value: 'data_analysis', label: t('agentEditor.tools.dataAnalysis'), description: t('agentEditor.tools.dataAnalysisDesc'), requiresKB: true }, - { value: 'data_schema', label: t('agentEditor.tools.dataSchema'), description: t('agentEditor.tools.dataSchemaDesc'), requiresKB: true }, + // 基础思考类 + { value: 'thinking', label: t('agentEditor.tools.thinking'), description: t('agentEditor.tools.thinkingDesc'), requiresKB: false, requiresRag: false, requiresWiki: false, group: 'base' }, + { value: 'todo_write', label: t('agentEditor.tools.todoWrite'), description: t('agentEditor.tools.todoWriteDesc'), requiresKB: false, requiresRag: false, requiresWiki: false, group: 'base' }, + // 知识库语义/关键词检索(均需 RAG 能力) + { value: 'grep_chunks', label: t('agentEditor.tools.grepChunks'), description: t('agentEditor.tools.grepChunksDesc'), requiresKB: true, requiresRag: true, requiresWiki: false, group: 'rag' }, + { value: 'knowledge_search', label: t('agentEditor.tools.knowledgeSearch'), description: t('agentEditor.tools.knowledgeSearchDesc'), requiresKB: true, requiresRag: true, requiresWiki: false, group: 'rag' }, + { value: 'list_knowledge_chunks', label: t('agentEditor.tools.listChunks'), description: t('agentEditor.tools.listChunksDesc'), requiresKB: true, requiresRag: true, requiresWiki: false, group: 'rag' }, + { value: 'query_knowledge_graph', label: t('agentEditor.tools.queryGraph'), description: t('agentEditor.tools.queryGraphDesc'), requiresKB: true, requiresRag: true, requiresWiki: false, group: 'rag' }, + { value: 'get_document_info', label: t('agentEditor.tools.getDocInfo'), description: t('agentEditor.tools.getDocInfoDesc'), requiresKB: true, requiresRag: true, requiresWiki: false, group: 'rag' }, + { value: 'database_query', label: t('agentEditor.tools.dbQuery'), description: t('agentEditor.tools.dbQueryDesc'), requiresKB: true, requiresRag: true, requiresWiki: false, group: 'rag' }, + // Wiki 读取类(阅读、搜索、标记问题) + { value: 'wiki_search', label: t('agentEditor.tools.wikiSearch'), description: t('agentEditor.tools.wikiSearchDesc'), requiresKB: true, requiresRag: false, requiresWiki: true, group: 'wiki_read' }, + { value: 'wiki_read_page', label: t('agentEditor.tools.wikiReadPage'), description: t('agentEditor.tools.wikiReadPageDesc'), requiresKB: true, requiresRag: false, requiresWiki: true, group: 'wiki_read' }, + { value: 'wiki_read_source_doc', label: t('agentEditor.tools.wikiReadSourceDoc'), description: t('agentEditor.tools.wikiReadSourceDocDesc'), requiresKB: true, requiresRag: false, requiresWiki: true, group: 'wiki_read' }, + { value: 'wiki_flag_issue', label: t('agentEditor.tools.wikiFlagIssue'), description: t('agentEditor.tools.wikiFlagIssueDesc'), requiresKB: true, requiresRag: false, requiresWiki: true, group: 'wiki_read' }, + // Wiki 编辑类(会直接修改 Wiki 内容) + { value: 'wiki_write_page', label: t('agentEditor.tools.wikiWritePage'), description: t('agentEditor.tools.wikiWritePageDesc'), requiresKB: true, requiresRag: false, requiresWiki: true, group: 'wiki_edit', danger: true }, + { value: 'wiki_replace_text', label: t('agentEditor.tools.wikiReplaceText'), description: t('agentEditor.tools.wikiReplaceTextDesc'), requiresKB: true, requiresRag: false, requiresWiki: true, group: 'wiki_edit', danger: true }, + { value: 'wiki_rename_page', label: t('agentEditor.tools.wikiRenamePage'), description: t('agentEditor.tools.wikiRenamePageDesc'), requiresKB: true, requiresRag: false, requiresWiki: true, group: 'wiki_edit', danger: true }, + { value: 'wiki_delete_page', label: t('agentEditor.tools.wikiDeletePage'), description: t('agentEditor.tools.wikiDeletePageDesc'), requiresKB: true, requiresRag: false, requiresWiki: true, group: 'wiki_edit', danger: true }, + // Wiki 巡检类 + { value: 'wiki_read_issue', label: t('agentEditor.tools.wikiReadIssue'), description: t('agentEditor.tools.wikiReadIssueDesc'), requiresKB: true, requiresRag: false, requiresWiki: true, group: 'wiki_issue' }, + { value: 'wiki_update_issue', label: t('agentEditor.tools.wikiUpdateIssue'), description: t('agentEditor.tools.wikiUpdateIssueDesc'), requiresKB: true, requiresRag: false, requiresWiki: true, group: 'wiki_issue' }, + // 数据分析(不依赖检索类能力,只需要作用域里有 KB 存放数据文件) + { value: 'data_analysis', label: t('agentEditor.tools.dataAnalysis'), description: t('agentEditor.tools.dataAnalysisDesc'), requiresKB: true, requiresRag: false, requiresWiki: false, group: 'data' }, + { value: 'data_schema', label: t('agentEditor.tools.dataSchema'), description: t('agentEditor.tools.dataSchemaDesc'), requiresKB: true, requiresRag: false, requiresWiki: false, group: 'data' }, +]); + +// 工具分组元信息 +const toolGroups = computed(() => [ + { key: 'base', label: t('agentEditor.tools.groupBase') }, + { key: 'rag', label: t('agentEditor.tools.groupRag') }, + { key: 'wiki_read', label: t('agentEditor.tools.groupWikiRead') }, + { key: 'wiki_edit', label: t('agentEditor.tools.groupWikiEdit') }, + { key: 'wiki_issue', label: t('agentEditor.tools.groupWikiIssue') }, + { key: 'data', label: t('agentEditor.tools.groupData') }, ]); // 知识库分组:我的 vs 共享的 @@ -1377,6 +1511,30 @@ const hasKnowledgeBase = computed(() => { return kbSelectionMode.value !== 'none'; }); +// 当前配置下进入到智能体作用域的知识库列表 +// 注意:用户可能选了 knowledge_bases(按库级),也可能选了 knowledge_ids(按文档级) +// 这里仅用于 UI 上的工具可用性判定,按库级来计算 +const kbsInScope = computed(() => { + if (kbSelectionMode.value === 'none') return []; + if (kbSelectionMode.value === 'all') return kbOptions.value; + const selectedIds = formData.value.config.knowledge_bases || []; + return kbOptions.value.filter(kb => selectedIds.includes(kb.value)); +}); + +// 是否存在至少一个启用了 RAG 能力的知识库(向量 or 关键词) +const hasRagKnowledgeBase = computed(() => { + return kbsInScope.value.some(kb => kb.ragEnabled); +}); + +// 是否存在至少一个启用了 Wiki 能力的知识库 +const hasWikiKnowledgeBase = computed(() => { + return kbsInScope.value.some(kb => kb.wikiEnabled); +}); + +// 作用域内 RAG/Wiki 知识库数量(用于顶部状态栏) +const ragKbCount = computed(() => kbsInScope.value.filter(kb => kb.ragEnabled).length); +const wikiKbCount = computed(() => kbsInScope.value.filter(kb => kb.wikiEnabled).length); + // 检测选择的知识库中是否包含 FAQ 类型 const hasFaqKnowledgeBase = computed(() => { if (kbSelectionMode.value === 'none') return false; @@ -1390,12 +1548,76 @@ const hasFaqKnowledgeBase = computed(() => { }); const availableTools = computed(() => { - return allTools.value.map(tool => ({ - ...tool, - disabled: tool.requiresKB && !hasKnowledgeBase.value - })); + return allTools.value.map(tool => { + // 通用知识库依赖:未配置任何知识库时禁用,优先级最高 + const needKbAndMissing = tool.requiresKB && !hasKnowledgeBase.value; + // Wiki 依赖:有 KB 但作用域内没有启用 Wiki 的 KB + const needWikiAndMissing = tool.requiresWiki && !hasWikiKnowledgeBase.value; + // RAG 依赖:有 KB 但作用域内没有启用向量/关键词索引的 KB + const needRagAndMissing = tool.requiresRag && !hasRagKnowledgeBase.value; + const disabled = needKbAndMissing || needWikiAndMissing || needRagAndMissing; + let disabledReason: string | undefined; + if (needKbAndMissing) { + disabledReason = t('agentEditor.tools.requiresKb'); + } else if (needWikiAndMissing) { + disabledReason = t('agentEditor.tools.requiresWikiKb'); + } else if (needRagAndMissing) { + disabledReason = t('agentEditor.tools.requiresRagKb'); + } + return { + ...tool, + disabled, + disabledReason, + }; + }); }); +// 按分组切片后的工具列表,用于模板分组渲染 +const groupedAvailableTools = computed(() => { + const map: Record = {}; + for (const tool of availableTools.value) { + const g = tool.group || 'base'; + if (!map[g]) map[g] = []; + map[g].push(tool); + } + return toolGroups.value + .map(g => ({ + ...g, + tools: map[g.key] || [], + })) + .filter(g => g.tools.length > 0); +}); + +// ==================== 有效工具预览 ==================== +// 最终运行时智能体实际能使用的工具集合(仅做预览展示) +// 规则:基于 allowed_tools 过滤 +// 1) 勾选但缺失对应能力(无 KB / 无 Wiki 能力 KB)的工具会被灰显/隐藏 +// 2) 无论是否勾选,web_search / web_fetch 随 web_search_enabled 出现 +// 3) final_answer 始终存在 +// 4) 当 kb_selection_mode === 'none' 时,RAG/Wiki 工具都视为不可用 +const effectiveTools = computed(() => { + const chosen = new Set(formData.value.config.allowed_tools || []); + const items: Array<{ value: string; label: string; reason?: string; active: boolean }> = []; + for (const tool of availableTools.value) { + const picked = chosen.has(tool.value); + if (!picked) continue; + if (tool.disabled) { + items.push({ value: tool.value, label: tool.label, active: false, reason: tool.disabledReason }); + } else { + items.push({ value: tool.value, label: tool.label, active: true }); + } + } + if (formData.value.config.web_search_enabled) { + items.push({ value: 'web_search', label: t('agentEditor.tools.webSearch'), active: true }); + items.push({ value: 'web_fetch', label: t('agentEditor.tools.webFetch'), active: true }); + } + items.push({ value: 'final_answer', label: t('agentEditor.tools.finalAnswer'), active: true }); + return items; +}); + +// 勾选了但当前配置下无法生效的工具数量(用于顶部状态提示) +const inactiveToolCount = computed(() => effectiveTools.value.filter(i => !i.active).length); + // 可用文件类型列表 const availableFileTypes = [ { value: 'pdf', label: 'PDF', description: t('agentEditor.fileTypes.pdf') }, @@ -1843,22 +2065,21 @@ watch(agentMode, (val, _oldVal) => { if (val === 'smart-reasoning') { // 切换到 Agent 模式,根据知识库配置启用工具 if (formData.value.config.allowed_tools.length === 0) { - if (hasKnowledgeBase.value) { - // 有知识库时,启用所有工具 - formData.value.config.allowed_tools = [ - 'thinking', - 'todo_write', + const tools = ['thinking', 'todo_write']; + if (hasRagKnowledgeBase.value) { + tools.push( 'knowledge_search', 'grep_chunks', 'list_knowledge_chunks', 'query_knowledge_graph', 'get_document_info', 'database_query', - ]; - } else { - // 没有知识库时,只启用非知识库工具 - formData.value.config.allowed_tools = ['thinking', 'todo_write']; + ); } + if (hasWikiKnowledgeBase.value) { + tools.push(...wikiReadTools); + } + formData.value.config.allowed_tools = tools; } if (formData.value.config.max_iterations <= 1) { formData.value.config.max_iterations = 10; @@ -1900,29 +2121,76 @@ watch(agentMode, (val, _oldVal) => { } }); -// 监听知识库配置变化,自动移除/添加知识库相关工具 +// 监听知识库配置变化,自动移除/添加 RAG 工具 watch(hasKnowledgeBase, (hasKB, oldHasKB) => { // 如果当前在检索策略页面但没有知识库能力了,切换到基础设置 if (!hasKB && currentSection.value === 'retrieval') { currentSection.value = 'basic'; } - + // 初始化期间或非 Agent 模式下不自动调整工具 if (isInitializing.value || !isAgentMode.value) return; - + if (hasKB && !oldHasKB) { // 从无知识库变为有知识库,自动添加知识库相关工具 const currentTools = formData.value.config.allowed_tools || []; const toolsToAdd = knowledgeBaseTools.filter((tool: string) => !currentTools.includes(tool)); formData.value.config.allowed_tools = [...currentTools, ...toolsToAdd]; } else if (!hasKB && oldHasKB) { - // 从有知识库变为无知识库,移除知识库相关工具 + // 从有知识库变为无知识库,移除所有 KB 依赖工具(含 Wiki) formData.value.config.allowed_tools = formData.value.config.allowed_tools.filter( - (tool: string) => !knowledgeBaseTools.includes(tool) + (tool: string) => !knowledgeBaseTools.includes(tool) && !allWikiTools.includes(tool) ); } }); +// 监听 Wiki 能力变化:当 Wiki 能力消失时移除已勾选的 Wiki 工具(避免"勾了但跑不起来") +watch(hasWikiKnowledgeBase, (hasWiki, oldHasWiki) => { + if (isInitializing.value || !isAgentMode.value) return; + if (!hasWiki && oldHasWiki) { + formData.value.config.allowed_tools = formData.value.config.allowed_tools.filter( + (tool: string) => !allWikiTools.includes(tool) + ); + } +}); + +// 检索策略预设:一键改写 allowed_tools +// auto -> 按作用域内知识库能力自动补齐(RAG 库 → RAG 工具;Wiki 库 → Wiki 读取工具) +// vector_only -> 只保留 RAG 工具,移除 Wiki 工具 +// wiki_only -> 只保留 Wiki 读取工具,移除 RAG 工具 +// hybrid -> 两者都补齐 +const onRetrievalPresetChange = (val: string) => { + formData.value.config.retrieval_preference = val as 'auto' | 'vector_only' | 'wiki_only' | 'hybrid'; + + if (!isAgentMode.value) return; + + const current = new Set(formData.value.config.allowed_tools || []); + + const addAll = (list: string[]) => list.forEach(t => current.add(t)); + const removeAll = (list: string[]) => list.forEach(t => current.delete(t)); + + switch (val) { + case 'vector_only': + if (hasRagKnowledgeBase.value) addAll(knowledgeBaseTools); + removeAll(allWikiTools); + break; + case 'wiki_only': + removeAll(knowledgeBaseTools); + if (hasWikiKnowledgeBase.value) addAll(wikiReadTools); + break; + case 'hybrid': + if (hasRagKnowledgeBase.value) addAll(knowledgeBaseTools); + if (hasWikiKnowledgeBase.value) addAll(wikiReadTools); + break; + case 'auto': + default: + if (hasRagKnowledgeBase.value) addAll(knowledgeBaseTools); + if (hasWikiKnowledgeBase.value) addAll(wikiReadTools); + break; + } + formData.value.config.allowed_tools = Array.from(current); +}; + // 监听运行模式变化,自动切换页面 watch(isAgentMode, (isAgent) => { // 如果当前在高级设置页面但切换到了Agent模式,切换到基础设置 @@ -3393,69 +3661,346 @@ const handleSave = async () => { } } -// 工具选择样式 -.tools-checkbox-group { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 12px; +// ===== 工具配置:overview 面板 ===== +.tools-overview { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 20px; + padding: 14px 16px; + background: var(--td-bg-color-secondarycontainer); + border-radius: 10px; + border: 1px solid var(--td-component-stroke); +} + +.tools-overview-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px 12px; + + &--preset { + border-top: 1px dashed var(--td-component-stroke); + padding-top: 10px; + justify-content: space-between; + } +} + +.tools-status-chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + font-size: 13px; + color: var(--td-text-color-secondary); + background: var(--td-bg-color-container); + border-radius: 999px; + border: 1px solid var(--td-component-stroke); + + .t-icon { + color: var(--td-text-color-secondary); + font-size: 14px; + } + + .tools-status-metric { + display: inline-flex; + align-items: baseline; + gap: 4px; + + strong { + font-size: 14px; + font-weight: 600; + color: var(--td-text-color-primary); + } + } + + .tools-status-sep { + color: var(--td-text-color-placeholder); + } + + &--warn { + color: var(--td-warning-color); + background: var(--td-warning-color-1, rgba(237, 118, 20, 0.08)); + border-color: var(--td-warning-color-light, #fcd7b6); + + .t-icon { color: var(--td-warning-color); } + } +} + +.tools-preset-label { + display: flex; + flex-direction: column; + min-width: 0; + gap: 2px; + + .tools-preset-title { + font-size: 13px; + font-weight: 600; + color: var(--td-text-color-primary); + } + + .tools-preset-hint { + font-size: 12px; + color: var(--td-text-color-secondary); + } +} + +// ===== 按组的卡片网格 ===== +.tool-groups { + display: flex; + flex-direction: column; + gap: 22px; width: 100%; } -.tool-checkbox-item { +.tool-group { display: flex; - align-items: flex-start; - padding: 12px 16px; - background: var(--td-bg-color-secondarycontainer); - border-radius: 8px; - border: 1px solid var(--td-component-stroke); - transition: all 0.2s ease; + flex-direction: column; + gap: 10px; +} - &:hover { - border-color: var(--td-brand-color); - background: var(--td-success-color-light); +.tool-group-header { + display: flex; + align-items: center; + gap: 8px; + padding-bottom: 2px; + + .tool-group-bar { + display: inline-block; + width: 3px; + height: 14px; + border-radius: 2px; + background: var(--td-brand-color); } + .tool-group-title { + font-size: 13px; + font-weight: 600; + color: var(--td-text-color-primary); + letter-spacing: 0.2px; + } + + .tool-group-count { + min-width: 20px; + padding: 0 6px; + font-size: 11px; + color: var(--td-text-color-secondary); + background: var(--td-bg-color-secondarycontainer); + border-radius: 999px; + text-align: center; + line-height: 18px; + } + + .tool-group-warning { + margin-left: auto; + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + font-size: 12px; + color: var(--td-warning-color); + background: var(--td-warning-color-1, rgba(237, 118, 20, 0.08)); + border: 1px solid var(--td-warning-color-light, #fcd7b6); + border-radius: 999px; + + .t-icon { font-size: 13px; } + } +} + +// 不同分组的左侧色条 +.tool-group--base .tool-group-bar { background: var(--td-gray-color-6, #a0a7ab); } +.tool-group--rag .tool-group-bar { background: var(--td-brand-color); } +.tool-group--wiki_read .tool-group-bar { background: var(--td-success-color, #2ba471); } +.tool-group--wiki_edit .tool-group-bar { background: var(--td-warning-color, #ed7b2f); } +.tool-group--wiki_issue .tool-group-bar { background: var(--td-purple-5, #8e56dd); } +.tool-group--data .tool-group-bar { background: var(--td-cyan-6, #09a3b7); } + +// 统一两列网格;小屏退化单列 +.tool-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + width: 100%; + + @media (max-width: 720px) { + grid-template-columns: 1fr; + } +} + +// ===== 工具卡片(基于 t-checkbox 的 label 结构) ===== +.tool-card { + margin: 0; // 清掉 TDesign checkbox 默认外边距 + padding: 12px 14px; + background: var(--td-bg-color-container); + border-radius: 8px; + border: 1px solid var(--td-component-stroke); + transition: border-color .2s, background .2s; + cursor: pointer; + overflow: hidden; + + &:hover:not(.tool-card--disabled) { + border-color: var(--td-brand-color); + background: var(--td-brand-color-1, rgba(0, 82, 217, 0.04)); + } + + // checkbox 的勾选框 + label 改造 :deep(.t-checkbox__input) { margin-top: 2px; + flex-shrink: 0; } :deep(.t-checkbox__label) { flex: 1; + min-width: 0; + padding-left: 10px; + } + + &.t-is-checked { + border-color: var(--td-brand-color); + background: var(--td-brand-color-1, rgba(0, 82, 217, 0.06)); + } + + &--disabled { + cursor: not-allowed; + opacity: 0.6; + } + + &--danger { + border-color: var(--td-warning-color-light, #fcd7b6); + + &:hover:not(.tool-card--disabled) { + border-color: var(--td-warning-color); + background: var(--td-warning-color-1, rgba(237, 118, 20, 0.06)); + } + + &.t-is-checked { + border-color: var(--td-warning-color); + background: var(--td-warning-color-1, rgba(237, 118, 20, 0.08)); + } } } -.tool-item-content { +.tool-card-body { display: flex; flex-direction: column; gap: 4px; + min-width: 0; } -.tool-name { - font-size: 14px; +.tool-card-head { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; +} + +.tool-card-name { + font-size: 13.5px; font-weight: 500; color: var(--td-text-color-primary); + line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 0 1 auto; + min-width: 0; } -.tool-desc { +.tool-card-badge { + flex: 0 0 auto; + font-size: 10.5px; + line-height: 1; + padding: 3px 6px; + color: var(--td-warning-color); + background: transparent; + border: 1px solid var(--td-warning-color-light, #fcd7b6); + border-radius: 4px; + letter-spacing: 0.3px; +} + +.tool-card-desc { font-size: 12px; color: var(--td-text-color-secondary); line-height: 1.5; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; } -.tool-disabled-hint { +.tool-card-hint { font-size: 11px; color: var(--td-warning-color); font-style: italic; + line-height: 1.4; } -.tool-disabled { - opacity: 0.6; - - .tool-name, .tool-desc { +.tool-card--disabled { + .tool-card-name, + .tool-card-desc { color: var(--td-text-color-placeholder); } } +// ===== 有效工具预览(芯片组)===== +.effective-tools { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding: 12px; + background: var(--td-bg-color-container); + border-radius: 8px; + border: 1px dashed var(--td-component-stroke); + min-height: 52px; + align-items: flex-start; +} + +.effective-chip { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 10px; + font-size: 12px; + line-height: 18px; + color: var(--td-brand-color); + background: var(--td-brand-color-1, rgba(0, 82, 217, 0.08)); + border: 1px solid var(--td-brand-color-2, rgba(0, 82, 217, 0.16)); + border-radius: 999px; + max-width: 100%; +} + +.effective-chip-label { + font-weight: 500; +} + +.effective-chip-reason { + font-size: 11px; + color: var(--td-warning-color); + font-style: normal; + + &::before { + content: "· "; + color: var(--td-text-color-placeholder); + margin-right: 2px; + } +} + +.effective-chip--inactive { + color: var(--td-text-color-placeholder); + background: var(--td-bg-color-secondarycontainer); + border-color: var(--td-component-stroke); + + .effective-chip-label { + text-decoration: line-through; + } +} + +.effective-tools-empty { + font-size: 12px; + color: var(--td-text-color-placeholder); + font-style: italic; +} + // Skills 选择样式 .skills-checkbox-group { display: grid; diff --git a/internal/application/service/agent_service.go b/internal/application/service/agent_service.go index 03e050651..d3d43cc03 100644 --- a/internal/application/service/agent_service.go +++ b/internal/application/service/agent_service.go @@ -25,6 +25,23 @@ import ( const MAX_ITERATIONS = 100 // Max iterations for agent execution +// dedupStrings removes duplicate strings while preserving the first occurrence order. +func dedupStrings(in []string) []string { + if len(in) == 0 { + return in + } + seen := make(map[string]struct{}, len(in)) + out := make([]string, 0, len(in)) + for _, s := range in { + if _, ok := seen[s]; ok { + continue + } + seen[s] = struct{}{} + out = append(out, s) + } + return out +} + // agentService implements agent-related business logic type agentService struct { cfg *config.Config @@ -323,9 +340,19 @@ func (s *agentService) registerTools( chatModel chat.Chat, sessionID string, ) error { - // Use config's allowed tools if specified, otherwise use defaults + // Source of truth policy: + // - `config.AllowedTools` is the explicit, user-editable whitelist. + // - We no longer silently *inject* tools the user didn't pick (esp. wiki tools). + // - We still *filter out* tools whose capability prerequisites are missing + // (no KB in scope, no Wiki-capable KB, etc.) so the LLM can't call tools + // that would error at runtime. + // - Legacy agents without AllowedTools fall back to DefaultAllowedTools(), + // and for them we preserve the historical "auto-append wiki tools when + // a wiki KB is in scope" behavior for backward compatibility. + hasExplicitAllowedTools := len(config.AllowedTools) > 0 + var allowedTools []string - if len(config.AllowedTools) > 0 { + if hasExplicitAllowedTools { allowedTools = make([]string, len(config.AllowedTools)) copy(allowedTools, config.AllowedTools) logger.Infof(ctx, "Using custom allowed tools from config: %v", allowedTools) @@ -334,47 +361,7 @@ func (s *agentService) registerTools( logger.Infof(ctx, "Using default allowed tools: %v", allowedTools) } - // Filter out knowledge base tools if no knowledge bases or knowledge IDs are configured - hasKnowledge := len(config.KnowledgeBases) > 0 || len(config.KnowledgeIDs) > 0 - if !hasKnowledge { - filteredTools := make([]string, 0) - kbTools := map[string]bool{ - tools.ToolKnowledgeSearch: true, - tools.ToolGrepChunks: true, - tools.ToolListKnowledgeChunks: true, - tools.ToolQueryKnowledgeGraph: true, - tools.ToolGetDocumentInfo: true, - tools.ToolDatabaseQuery: true, - tools.ToolDataAnalysis: true, - tools.ToolDataSchema: true, - } - - // If no knowledge and no web search, also disable todo_write (not useful for simple chat) - if !config.WebSearchEnabled { - kbTools[tools.ToolTodoWrite] = true - } - - for _, toolName := range allowedTools { - if !kbTools[toolName] { - filteredTools = append(filteredTools, toolName) - } - } - allowedTools = filteredTools - logger.Infof(ctx, "Pure Agent Mode: Knowledge base tools filtered out, remaining: %v", allowedTools) - } - - // If web search is enabled, add web_search to allowedTools - if config.WebSearchEnabled { - allowedTools = append(allowedTools, tools.ToolWebSearch) - allowedTools = append(allowedTools, tools.ToolWebFetch) - } - - // Detect KB capabilities for retrieval preference logic - retrievalPref := config.RetrievalPreference - if retrievalPref == "" { - retrievalPref = "auto" - } - + // ---- Capability detection from SearchTargets ---- var hasVectorKB, hasWikiKB bool var wikiKBIDs []string var wikiScopes []tools.WikiScope @@ -400,57 +387,175 @@ func (s *agentService) registerTools( } } - // Apply retrieval preference to tool list + // Filter out knowledge base tools if no knowledge bases or knowledge IDs are configured + hasKnowledge := len(config.KnowledgeBases) > 0 || len(config.KnowledgeIDs) > 0 + if !hasKnowledge { + filteredTools := make([]string, 0) + kbTools := map[string]bool{ + tools.ToolKnowledgeSearch: true, + tools.ToolGrepChunks: true, + tools.ToolListKnowledgeChunks: true, + tools.ToolQueryKnowledgeGraph: true, + tools.ToolGetDocumentInfo: true, + tools.ToolDatabaseQuery: true, + tools.ToolDataAnalysis: true, + tools.ToolDataSchema: true, + // Wiki tools also require at least one KB in scope. + tools.ToolWikiReadPage: true, + tools.ToolWikiSearch: true, + tools.ToolWikiReadSourceDoc: true, + tools.ToolWikiFlagIssue: true, + tools.ToolWikiWritePage: true, + tools.ToolWikiReplaceText: true, + tools.ToolWikiRenamePage: true, + tools.ToolWikiDeletePage: true, + tools.ToolWikiReadIssue: true, + tools.ToolWikiUpdateIssue: true, + } + + // If no knowledge and no web search, also disable todo_write (not useful for simple chat) + if !config.WebSearchEnabled { + kbTools[tools.ToolTodoWrite] = true + } + + for _, toolName := range allowedTools { + if !kbTools[toolName] { + filteredTools = append(filteredTools, toolName) + } + } + allowedTools = filteredTools + logger.Infof(ctx, "Pure Agent Mode: Knowledge base tools filtered out, remaining: %v", allowedTools) + } + + // If web search is enabled, add web_search to allowedTools + if config.WebSearchEnabled { + allowedTools = append(allowedTools, tools.ToolWebSearch) + allowedTools = append(allowedTools, tools.ToolWebFetch) + } + + retrievalPref := config.RetrievalPreference + if retrievalPref == "" { + retrievalPref = "auto" + } + + // Default wiki read-only set used for backward-compat auto-injection below. + // Edit/issue wiki tools are intentionally excluded — they must be explicitly + // opted in via AllowedTools because they can mutate data. + defaultWikiReadTools := []string{ + tools.ToolWikiReadPage, + tools.ToolWikiSearch, + tools.ToolWikiReadSourceDoc, + tools.ToolWikiFlagIssue, + } + + // ---- Apply retrieval preference ---- + // Preference is now a *filter*, never an injector (for explicit lists). + // For legacy agents without explicit AllowedTools, we still auto-append + // wiki read tools so existing integrations keep working. + // + // NOTE: ragToolSet must stay in sync with frontend `knowledgeBaseTools` + // in AgentEditorModal.vue. These are *all* tools that retrieve/inspect + // content from RAG-style knowledge bases; wiki_only must strip the whole + // set, otherwise the agent can still reach RAG data via graph/db/doc + // info tools despite the user asking for Wiki only. + ragToolSet := map[string]bool{ + tools.ToolKnowledgeSearch: true, + tools.ToolGrepChunks: true, + tools.ToolListKnowledgeChunks: true, + tools.ToolQueryKnowledgeGraph: true, + tools.ToolGetDocumentInfo: true, + tools.ToolDatabaseQuery: true, + } + allWikiToolSet := map[string]bool{ + tools.ToolWikiReadPage: true, + tools.ToolWikiSearch: true, + tools.ToolWikiReadSourceDoc: true, + tools.ToolWikiFlagIssue: true, + tools.ToolWikiWritePage: true, + tools.ToolWikiReplaceText: true, + tools.ToolWikiRenamePage: true, + tools.ToolWikiDeletePage: true, + tools.ToolWikiReadIssue: true, + tools.ToolWikiUpdateIssue: true, + } + switch retrievalPref { case "vector_only": - // Keep vector tools, no wiki tools (even if KB has wiki) - logger.Infof(ctx, "Retrieval preference: vector_only, skipping wiki tools") + filtered := make([]string, 0, len(allowedTools)) + dropped := make([]string, 0) + for _, t := range allowedTools { + if allWikiToolSet[t] { + dropped = append(dropped, t) + continue + } + filtered = append(filtered, t) + } + allowedTools = filtered + logger.Infof(ctx, "Retrieval preference: vector_only, wiki tools filtered out: %v", dropped) case "wiki_only": - // Remove vector search tools, add wiki tools if available - if hasWikiKB { - vectorSearchTools := map[string]bool{ - tools.ToolKnowledgeSearch: true, - tools.ToolGrepChunks: true, - tools.ToolListKnowledgeChunks: true, + filtered := make([]string, 0, len(allowedTools)) + dropped := make([]string, 0) + for _, t := range allowedTools { + if ragToolSet[t] { + dropped = append(dropped, t) + continue } - filteredTools := make([]string, 0, len(allowedTools)) - for _, t := range allowedTools { - if !vectorSearchTools[t] { - filteredTools = append(filteredTools, t) - } - } - allowedTools = filteredTools - allowedTools = append(allowedTools, - tools.ToolWikiReadPage, - tools.ToolWikiSearch, - tools.ToolWikiReadSourceDoc, - tools.ToolWikiFlagIssue, - ) - logger.Infof(ctx, "Retrieval preference: wiki_only, wiki tools added, vector tools removed") + filtered = append(filtered, t) } + allowedTools = filtered + // Only legacy agents get auto-append here; explicit lists are authoritative. + if !hasExplicitAllowedTools && hasWikiKB { + allowedTools = append(allowedTools, defaultWikiReadTools...) + } + logger.Infof(ctx, "Retrieval preference: wiki_only, RAG tools filtered out: %v", dropped) case "hybrid": - // Keep vector tools AND add wiki tools - if hasWikiKB { - allowedTools = append(allowedTools, - tools.ToolWikiReadPage, - tools.ToolWikiSearch, - tools.ToolWikiReadSourceDoc, - tools.ToolWikiFlagIssue, - ) - logger.Infof(ctx, "Retrieval preference: hybrid, both vector and wiki tools") + if !hasExplicitAllowedTools && hasWikiKB { + allowedTools = append(allowedTools, defaultWikiReadTools...) + logger.Infof(ctx, "Retrieval preference: hybrid (legacy), wiki read tools auto-added") } - default: // "auto" — preserve original behavior: auto-detect from KB capabilities - if len(wikiKBIDs) > 0 { - allowedTools = append(allowedTools, - tools.ToolWikiReadPage, - tools.ToolWikiSearch, - tools.ToolWikiReadSourceDoc, - tools.ToolWikiFlagIssue, - ) - logger.Infof(ctx, "Retrieval preference: auto, wiki KBs detected (%d), wiki tools added", len(wikiKBIDs)) + default: // "auto" + if !hasExplicitAllowedTools && hasWikiKB { + allowedTools = append(allowedTools, defaultWikiReadTools...) + logger.Infof(ctx, "Retrieval preference: auto (legacy), wiki read tools auto-added") } } - _ = hasVectorKB // used for future optimization + + // Hard safety nets: drop tools whose runtime prerequisite is missing. + // This guards against stale configs where e.g. the user ticked wiki tools + // earlier but later swapped in a non-wiki KB (or vice versa for RAG). + if !hasWikiKB { + filtered := make([]string, 0, len(allowedTools)) + dropped := make([]string, 0) + for _, t := range allowedTools { + if allWikiToolSet[t] { + dropped = append(dropped, t) + continue + } + filtered = append(filtered, t) + } + allowedTools = filtered + if len(dropped) > 0 { + logger.Warnf(ctx, "Dropped wiki tools %v because no wiki-capable KB is in scope", dropped) + } + } + if !hasVectorKB { + filtered := make([]string, 0, len(allowedTools)) + dropped := make([]string, 0) + for _, t := range allowedTools { + if ragToolSet[t] { + dropped = append(dropped, t) + continue + } + filtered = append(filtered, t) + } + allowedTools = filtered + if len(dropped) > 0 { + logger.Warnf(ctx, "Dropped RAG tools %v because no RAG-capable KB is in scope", dropped) + } + } + + // Deduplicate while preserving original order. + allowedTools = dedupStrings(allowedTools) logger.Infof(ctx, "Registering tools: %v, webSearchEnabled: %v", allowedTools, config.WebSearchEnabled) allowedTools = append(allowedTools, tools.ToolFinalAnswer)