From 995e2157cdb70d3af4a926aeaffe2c41a8a5a4c5 Mon Sep 17 00:00:00 2001 From: wizardchen Date: Fri, 10 Apr 2026 21:04:55 +0800 Subject: [PATCH] feat: Enhance Wiki Fixer agent with improved issue handling and UI updates - Updated the agent system prompt to refine the workflow for fixing issues, emphasizing the need to verify if issues still exist before making edits. - Introduced a new `embeddedMode` prop in the frontend components to manage UI behavior based on the context of use. - Simplified issue fix prompts in multiple languages for clarity, ensuring users receive concise instructions for resolving issues. - Enhanced the WikiBrowser component to improve the display of issues and actions, including updated icons and streamlined interaction elements. These changes significantly improve the user experience and functionality of the Wiki Fixer agent, fostering more efficient issue resolution and content management. --- .../prompt_templates/agent_system_prompt.yaml | 38 +++--- frontend/src/components/Input-field.vue | 6 +- frontend/src/i18n/locales/en-US.ts | 6 +- frontend/src/i18n/locales/ko-KR.ts | 6 +- frontend/src/i18n/locales/ru-RU.ts | 6 +- frontend/src/i18n/locales/zh-CN.ts | 6 +- frontend/src/views/chat/index.vue | 20 ++-- .../src/views/knowledge/wiki/WikiBrowser.vue | 113 +++++++++--------- internal/agent/prompts_wiki.go | 4 +- internal/agent/tools/wiki_replace_text.go | 18 +-- internal/agent/tools/wiki_tools.go | 41 ++++++- internal/agent/tools/wiki_write_page.go | 22 ++-- internal/application/service/agent_service.go | 4 +- 13 files changed, 170 insertions(+), 120 deletions(-) diff --git a/config/prompt_templates/agent_system_prompt.yaml b/config/prompt_templates/agent_system_prompt.yaml index fe6109fc3..cfebcae56 100644 --- a/config/prompt_templates/agent_system_prompt.yaml +++ b/config/prompt_templates/agent_system_prompt.yaml @@ -301,45 +301,51 @@ templates: You must follow these steps for EVERY fixing task: - 1. **Understand the Issue:** The user will provide you with the target page slug, issue description, and optionally suspected knowledge IDs. Use `wiki_read_issue` if you need to list other pending issues. - 2. **Read the Current State:** Call `wiki_read_page` on the target slug to see the current content and related links. Use `wiki_search` if you need to find related entities or check if a page already exists for separated content. - 3. **Investigate Sources:** If the issue involves conflicting facts or mixed entities, the current wiki page might be poisoned. You MUST call `wiki_read_source_doc` using the `knowledge_id`s listed in the page's `` or provided in the issue description. Read the raw text to discover the truth. - 4. **Determine the Fix Strategy:** + 1. **Read the Issue:** The user will provide you with one or more issue IDs and a target page slug. Your FIRST action MUST be to call `wiki_read_issue` with the provided issue ID(s) to get the full issue details (type, description, suspected sources, etc.). Do NOT skip this step. + 2. **Read the Current State:** Call `wiki_read_page` on the target slug to see the current content and related links. You can call `wiki_read_issue` and `wiki_read_page` in parallel. Use `wiki_search` if you need to find related entities or check if a page already exists for separated content. + 3. **Verify the Issue Still Exists:** After reading the issue details and the current page content, CHECK whether the problem described in the issue actually exists in the current page. The issue might have already been fixed by a previous edit. If the issue no longer applies: + - Inform the user that the issue appears to be already resolved. + - Call `wiki_update_issue` to mark the issue as "resolved" with a note. + - Do NOT make any edits to the page. + - Stop here — do not continue to steps 4-7. + 4. **Investigate Sources:** If the issue is confirmed to still exist and involves conflicting facts or mixed entities, the current wiki page might be poisoned. You MUST call `wiki_read_source_doc` using the `knowledge_id`s listed in the page's `` or provided in the issue description. Read the raw text to discover the truth. + 5. **Determine the Fix Strategy:** - *Correction:* Fix minor errors efficiently using the `wiki_replace_text` tool, or rewrite the page using `wiki_write_page`. - *Renaming:* If the page title or slug is fundamentally wrong, use `wiki_rename_page` to change the slug. Incoming links will be updated automatically! - *Separation (Disambiguation):* Rewrite the target page to only focus on its true subject (using `wiki_write_page`), and remove the competitor's info. - *Creation:* Create a new page for the separated entity using `wiki_write_page`. - *Deletion:* If a page is completely redundant or should not exist, use `wiki_delete_page`. Incoming links will be cleaned up automatically! - 5. **Plan & Confirm:** Before calling any write/replace/rename/delete tool, you MUST present a plan of what you intend to change to the user and ask for their confirmation. Wait for the user to say "yes" or "approved" or express general agreement before making the actual edits. You should be robust to conversational affirmative responses (e.g. "好的", "ok", "go ahead", "没问题") instead of requiring exact phrase matches. - 6. **Apply the Fix:** After user confirmation, use the appropriate tool. + 6. **Announce & Execute:** Briefly announce what you are going to do (1-2 sentences), then IMMEDIATELY apply the fix using the appropriate tools in the SAME turn. Do NOT wait for user confirmation — the user has already requested the fix by clicking the "Fix" button. Execute everything in a single turn. - For `wiki_replace_text`, provide the exact `old_text` and the `new_text`. - For `wiki_rename_page`, provide the `new_slug`. - For `wiki_write_page`, provide the `title`, a concise 1-sentence `summary` for the index, the `page_type`, and the FULL, complete, corrected Markdown `content`. Do not output diffs in `content`. - For `wiki_delete_page`, just provide the `slug`. - 7. **Update Issue Status:** Use `wiki_update_issue` to mark the issue as "resolved" after successfully editing the page. + 7. **Update Issue Status:** After all edits are applied, use `wiki_update_issue` to mark each issue as "resolved". ABSOLUTE RULES: 1. **Never Guess:** Always base your fixes on evidence found in the raw source documents (`wiki_read_source_doc`). - 2. **Require Confirmation:** NEVER edit without first presenting your plan and getting explicit approval from the user in the conversation. Accept variations of affirmative responses (like "ok", "好的", "没问题", "可以"). - 3. **Complete Content for Write:** When using `wiki_write_page`, you must provide the ENTIRE page content. Do not truncate or use placeholders like "...rest of the content...". Also, NEVER forget to provide the one-sentence `summary` field. - 4. **Exact Match for Replace:** When using `wiki_replace_text`, the `old_text` must EXACTLY match the text currently in the page. - 5. **Maintain Links:** When rewriting a page, try to preserve valid Wiki links `[[slug|Display Name]]`. - 6. **Writing Style:** You must strictly follow the standard Wiki writing style: + 2. **Read Issue First:** The user message only contains issue IDs. You MUST call `wiki_read_issue` to get the actual issue details before doing anything else. + 3. **Do Not Force Fix:** If after investigation the issue no longer exists in the current page (already fixed or no longer applicable), do NOT make any edits. Just mark the issue as resolved and inform the user. + 4. **No Confirmation Needed:** The user has explicitly requested the fix. Do NOT ask for confirmation or wait for a second message. Investigate → plan → execute → done, all in ONE turn. + 5. **Complete Content for Write:** When using `wiki_write_page`, you must provide the ENTIRE page content. Do not truncate or use placeholders like "...rest of the content...". Also, NEVER forget to provide the one-sentence `summary` field. + 6. **Exact Match for Replace:** When using `wiki_replace_text`, the `old_text` must EXACTLY match the text currently in the page. + 7. **Maintain Links:** When rewriting a page, try to preserve valid Wiki links `[[slug|Display Name]]`. + 8. **Writing Style:** You must strictly follow the standard Wiki writing style: - Use proper heading hierarchy (`##` for sections, `###` for subsections). - Include a "## Key Takeaways" section with bullet points at the end. - Preserve any valid image links `![alt](url)`. - 7. **Source Refs:** When calling `wiki_write_page` or `wiki_replace_text`, you MUST provide the `source_refs` array containing the `knowledge_id`s of the source documents you used to verify the information. + 9. **Source Refs:** When calling `wiki_write_page` or `wiki_replace_text`, you MUST provide the `source_refs` array containing the `knowledge_id`s of the source documents you used to verify the information. - * **wiki_read_page:** Use this first to see what the broken page looks like. + * **wiki_read_issue:** Call this FIRST to read the full issue details from the provided issue ID(s). This is mandatory — the user message only contains IDs, not the full description. + * **wiki_read_page:** Use this to see the current state of the broken page. Can be called in parallel with `wiki_read_issue`. * **wiki_search:** Use this to explore the wiki if you need to find related concepts or verify if another page already exists. * **wiki_read_source_doc:** Use this to find the ground truth. It is crucial for resolving "contradictory_facts" or "mixed_entities" issues. - * **wiki_read_issue:** Use this to read specific issue details or list pending issues for a page. * **todo_write:** Use this to write down the plan and modifications you intend to make, so that you can remember them across conversation turns, and present them to the user. - * **wiki_write_page / wiki_replace_text / wiki_rename_page / wiki_delete_page:** Use these to apply your fix ONLY AFTER the user confirms your plan. + * **wiki_write_page / wiki_replace_text / wiki_rename_page / wiki_delete_page:** Use these to apply your fix directly after investigation. No user confirmation is needed. * **wiki_update_issue:** Use this to set the issue status to "resolved" after the page is fixed. diff --git a/frontend/src/components/Input-field.vue b/frontend/src/components/Input-field.vue index 7a2e66f11..213160ca3 100644 --- a/frontend/src/components/Input-field.vue +++ b/frontend/src/components/Input-field.vue @@ -285,6 +285,10 @@ const props = defineProps({ assistantMessageId: { type: String, required: false + }, + embeddedMode: { + type: Boolean, + default: false } }); @@ -1950,7 +1954,7 @@ defineExpose({
-
+
{ suggestedQuestionsLoading.value = true; // 加载期间保留旧数据,不清空,避免布局抖动 try { - const agentId = useSettingsStoreInstance.selectedAgentId; + const agentId = props.embeddedMode ? props.agentId : useSettingsStoreInstance.selectedAgentId; if (!agentId) return; - const selectedKBs = useSettingsStoreInstance.getSelectedKnowledgeBases(); - const selectedFiles = useSettingsStoreInstance.getSelectedFiles(); + const selectedKBs = props.embeddedMode ? props.kbIds : useSettingsStoreInstance.getSelectedKnowledgeBases(); + const selectedFiles = props.embeddedMode ? [] : useSettingsStoreInstance.getSelectedFiles(); const res = await getSuggestedQuestions(agentId, { knowledge_base_ids: selectedKBs.length > 0 ? selectedKBs : undefined, knowledge_ids: selectedFiles.length > 0 ? selectedFiles : undefined, @@ -539,18 +539,18 @@ const sendMsg = async (value, modelId = '', mentionedItems = [], imageFiles = [] scrollToBottom(true); // Get agent mode status from settings store - const agentEnabled = useSettingsStoreInstance.isAgentEnabled; + const agentEnabled = props.embeddedMode ? (props.agentId && props.agentId !== 'builtin-quick-answer') : useSettingsStoreInstance.isAgentEnabled; // Get web search status from settings store - const webSearchEnabled = useSettingsStoreInstance.isWebSearchEnabled; + const webSearchEnabled = props.embeddedMode ? false : useSettingsStoreInstance.isWebSearchEnabled; // Get memory status from settings store - const enableMemory = useSettingsStoreInstance.isMemoryEnabled; + const enableMemory = props.embeddedMode ? false : useSettingsStoreInstance.isMemoryEnabled; // Get knowledge_base_ids from settings store (selected by user via KnowledgeBaseSelector) // Merge @mentioned KB/file IDs so retrieval uses the same targets user @mentioned (including shared KBs) - const sidebarKbIds = useSettingsStoreInstance.settings.selectedKnowledgeBases || []; - const sidebarFileIds = useSettingsStoreInstance.settings.selectedFiles || []; + const sidebarKbIds = props.embeddedMode ? props.kbIds : (useSettingsStoreInstance.settings.selectedKnowledgeBases || []); + const sidebarFileIds = props.embeddedMode ? [] : (useSettingsStoreInstance.settings.selectedFiles || []); const kbIdSet = new Set(sidebarKbIds); const fileIdSet = new Set(sidebarFileIds); for (const item of mentionedItems || []) { @@ -565,13 +565,13 @@ const sendMsg = async (value, modelId = '', mentionedItems = [], imageFiles = [] const knowledgeIds = [...fileIdSet]; // Get selected agent ID (backend resolves shared agent and its tenant from share relation) - const selectedAgentId = useSettingsStoreInstance.selectedAgentId || ''; + const selectedAgentId = props.embeddedMode ? props.agentId : (useSettingsStoreInstance.selectedAgentId || ''); // Use agent-chat endpoint when agent is enabled, otherwise use knowledge-chat const endpoint = agentEnabled ? '/api/v1/agent-chat' : '/api/v1/knowledge-chat'; // Get selected MCP services from settings store (if available) - const mcpServiceIds = useSettingsStoreInstance.settings.selectedMCPServices || []; + const mcpServiceIds = props.embeddedMode ? [] : (useSettingsStoreInstance.settings.selectedMCPServices || []); await startStream({ session_id: session_id.value, diff --git a/frontend/src/views/knowledge/wiki/WikiBrowser.vue b/frontend/src/views/knowledge/wiki/WikiBrowser.vue index 6b97adc77..162118427 100644 --- a/frontend/src/views/knowledge/wiki/WikiBrowser.vue +++ b/frontend/src/views/knowledge/wiki/WikiBrowser.vue @@ -232,14 +232,12 @@ trigger="click" :overlayInnerStyle="{ padding: 0, boxShadow: 'var(--td-shadow-3)', borderRadius: '8px', width: '560px', maxWidth: '90vw' }" > - - - {{ $t('knowledgeEditor.wikiBrowser.issueTitle', { count: pageIssues.length }) }} - + + @@ -377,10 +377,12 @@ {{ issue.reported_by === 'wiki-researcher-agent' ? $t('knowledgeEditor.wikiBrowser.issueAiLinter') : $t('knowledgeEditor.wikiBrowser.issueReportedBy', { reporter: issue.reported_by }) }} - - {{ $t('knowledgeEditor.wikiBrowser.issueGoFix') }} - - {{ $t('knowledgeEditor.wikiBrowser.issueIgnore') }} +
+ + {{ $t('knowledgeEditor.wikiBrowser.issueGoFix') }} + + {{ $t('knowledgeEditor.wikiBrowser.issueIgnore') }} +
@@ -882,21 +884,8 @@ async function handleIssueIgnore(issueId: string) { } async function startFixSession(prompt: string) { - settingsStore.selectAgent("builtin-wiki-fixer", null) - - const sessionData: any = { - agent_config: { - enabled: true, - max_iterations: 30, - temperature: 0.7, - knowledge_bases: [props.knowledgeBaseId], - knowledge_ids: [], - allowed_tools: ["thinking", "wiki_read_page", "wiki_search", "wiki_read_source_doc", "wiki_edit_page", "wiki_read_issue", "wiki_update_issue"] - } - } - try { - const res = await createSessions(sessionData) + const res = await createSessions({}) if (res && (res as any).data && (res as any).data.id) { const sessionId = (res as any).data.id const now = new Date().toISOString() @@ -930,8 +919,6 @@ function triggerFixIssue(issue: WikiPageIssue) { if (!selectedPage.value) return const prompt = t('knowledgeEditor.wikiBrowser.issueFixPromptSingle', { slug: selectedPage.value.slug, - type: issue.issue_type, - desc: issue.description, id: issue.id }) startFixSession(prompt) @@ -942,10 +929,9 @@ function triggerAutoFix() { let prompt = t('knowledgeEditor.wikiBrowser.issueFixPromptAutoStart', { slug: selectedPage.value.slug }) + '\n\n' pageIssues.value.forEach((issue, idx) => { - prompt += `${idx + 1}. [${issue.issue_type}] ${issue.description} (ID: ${issue.id})\n` + prompt += `${idx + 1}. Issue ID: ${issue.id}\n` }) - prompt += '\n' + t('knowledgeEditor.wikiBrowser.issueFixPromptAutoEnd') startFixSession(prompt) } @@ -2446,13 +2432,16 @@ onUnmounted(() => { // ── Issues Popup ── .wiki-issue-trigger { - margin-left: 12px; + margin-left: 8px; cursor: pointer; - transition: all 0.2s ease; - user-select: none; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + transition: opacity 0.2s ease; &:hover { - filter: brightness(0.95); + opacity: 0.8; } } @@ -2492,21 +2481,21 @@ onUnmounted(() => { flex-direction: column; max-height: 400px; overflow-y: auto; + gap: 12px; + padding: 8px 12px; } .wiki-issue-popup-item { display: flex; padding: 16px; gap: 12px; - border-bottom: 1px solid var(--td-component-stroke); - transition: background-color 0.2s ease; - - &:last-child { - border-bottom: none; - } + border: 1px solid var(--td-component-border); + border-radius: 6px; + transition: box-shadow 0.2s ease, border-color 0.2s ease; + background: var(--td-bg-color-container); &:hover { - background: var(--td-bg-color-container-hover); + border-color: var(--td-brand-color-light); } } @@ -2550,12 +2539,20 @@ onUnmounted(() => { display: flex; align-items: center; gap: 16px; - margin-top: 4px; + margin-top: 8px; + padding-top: 12px; + border-top: 1px dashed var(--td-component-stroke); } .wiki-issue-popup-reporter { font-size: 12px; color: var(--td-text-color-placeholder); + flex: 1; +} + +.wiki-issue-popup-actions { + display: flex; + align-items: center; } .wiki-issue-popup-action { diff --git a/internal/agent/prompts_wiki.go b/internal/agent/prompts_wiki.go index b3313f9cf..08e78abe3 100644 --- a/internal/agent/prompts_wiki.go +++ b/internal/agent/prompts_wiki.go @@ -65,7 +65,7 @@ If previous slugs are provided above, you MUST follow these rules: Each entity should have: - "name": The entity name in {{.Language}} (human-readable) - "slug": URL-friendly slug, format "entity/" (use romanized/pinyin form for non-Latin names). **Reuse previous slug if the entity was extracted before.** -- "aliases": An array of strings representing alternative names, abbreviations, acronyms or translations of the entity found in the document. Provide [] if none. +- "aliases": An array of strings representing names that refer to THE EXACT SAME entity. Only include: official abbreviations (e.g. "IBM" for "International Business Machines"), full/short name variants (e.g. "腾讯" for "腾讯控股有限公司"), translations (e.g. "Apple" for "苹果公司"), and well-known alternate names (e.g. "Alphabet" for "Google母公司"). Do NOT include parent categories, related products, generic terms, or broader concepts. Provide [] if none. - "description": **Index listing summary** — one sentence, 15-40 words, in {{.Language}}. Describes WHAT this entity IS and its role in the document. Must be self-contained (understandable without reading the full page). This will be displayed in the wiki index. - "details": A 2-5 sentence summary in {{.Language}} of key facts from the document. **Image rule**: If the document contains relevant elements in an tag, include them in the details using Markdown syntax: ![caption](url). @@ -75,7 +75,7 @@ Only include entities that are substantively discussed (mentioned at least twice Each concept should have: - "name": The concept name in {{.Language}} (human-readable) - "slug": URL-friendly slug, format "concept/" (use romanized/pinyin form for non-Latin names). **Reuse previous slug if the concept was extracted before.** -- "aliases": An array of strings representing alternative names, abbreviations, acronyms or translations of the concept found in the document. Provide [] if none. +- "aliases": An array of strings representing names that refer to THE EXACT SAME concept. Only include: official abbreviations (e.g. "RAG" for "Retrieval-Augmented Generation"), full/short name variants, and well-known synonyms used interchangeably in the field. Do NOT include sub-topics, related techniques, broader categories, or implementation details. Provide [] if none. - "description": **Index listing summary** — one sentence, 15-40 words, in {{.Language}}. Defines WHAT this concept IS. Must be self-contained (understandable without reading the full page). This will be displayed in the wiki index. - "details": A 2-5 sentence explanation in {{.Language}} as discussed in the document. **Image rule**: If the document contains relevant elements in an tag, include them in the details using Markdown syntax: ![caption](url). diff --git a/internal/agent/tools/wiki_replace_text.go b/internal/agent/tools/wiki_replace_text.go index cc7514503..4f66a0ac6 100644 --- a/internal/agent/tools/wiki_replace_text.go +++ b/internal/agent/tools/wiki_replace_text.go @@ -12,12 +12,13 @@ import ( type wikiReplaceTextTool struct { BaseTool - wikiPageService interfaces.WikiPageService - kbIDs []string + wikiPageService interfaces.WikiPageService + knowledgeService interfaces.KnowledgeService + kbIDs []string } // NewWikiReplaceTextTool creates a new wiki_replace_text tool -func NewWikiReplaceTextTool(wikiPageService interfaces.WikiPageService, kbIDs []string) types.Tool { +func NewWikiReplaceTextTool(wikiPageService interfaces.WikiPageService, kbIDs []string, knowledgeService interfaces.KnowledgeService) types.Tool { return &wikiReplaceTextTool{ BaseTool: NewBaseTool( ToolWikiReplaceText, @@ -40,14 +41,15 @@ func NewWikiReplaceTextTool(wikiPageService interfaces.WikiPageService, kbIDs [] "source_refs": { "type": "array", "items": {"type": "string"}, - "description": "An optional list of source knowledge IDs that justify this change. If provided, these will COMPLETELY REPLACE the existing source_refs of the page." + "description": "An optional list of source knowledge IDs (UUIDs only) that justify this change. If provided, these will COMPLETELY REPLACE the existing source_refs of the page." } }, "required": ["slug", "old_text", "new_text"] }`), ), - wikiPageService: wikiPageService, - kbIDs: kbIDs, + wikiPageService: wikiPageService, + knowledgeService: knowledgeService, + kbIDs: kbIDs, } } @@ -85,13 +87,13 @@ func (t *wikiReplaceTextTool) Execute(ctx context.Context, args json.RawMessage) existingPage.Content = strings.Replace(existingPage.Content, params.OldText, params.NewText, 1) if len(params.SourceRefs) > 0 { - existingPage.SourceRefs = params.SourceRefs + existingPage.SourceRefs = resolveSourceRefs(ctx, t.knowledgeService, params.SourceRefs) } _, err = t.wikiPageService.UpdatePage(ctx, existingPage) if err != nil { return &types.ToolResult{Success: false, Error: "Failed to update page: " + err.Error()}, nil } - + return &types.ToolResult{Success: true, Output: fmt.Sprintf("Successfully replaced text on page %s.", params.Slug)}, nil } diff --git a/internal/agent/tools/wiki_tools.go b/internal/agent/tools/wiki_tools.go index f66f68e5e..7e7433b3c 100644 --- a/internal/agent/tools/wiki_tools.go +++ b/internal/agent/tools/wiki_tools.go @@ -137,6 +137,7 @@ func (t *wikiReadPageTool) Execute(ctx context.Context, args json.RawMessage) (* %s %s %s +%s %s @@ -153,6 +154,7 @@ func (t *wikiReadPageTool) Execute(ctx context.Context, args json.RawMessage) (* `, page.Title, page.Slug, page.PageType, + strings.Join(page.Aliases, ", "), strings.Join(outLinksDesc, ", "), strings.Join(inLinksDesc, ", "), strings.Join(sourcesDesc, "\n"), @@ -278,10 +280,15 @@ func (t *wikiSearchTool) Execute(ctx context.Context, args json.RawMessage) (*ty snippetTag = fmt.Sprintf("\n%s", snippet) } + aliasesTag := "" + if len(p.Aliases) > 0 { + aliasesTag = fmt.Sprintf("\n%s", strings.Join(p.Aliases, ", ")) + } + if seen { - fmt.Fprintf(&sb, "\n%s\n%s\n%s\n(summary omitted, already seen in previous search)%s\n\n", p.Title, p.Slug, p.PageType, snippetTag) + fmt.Fprintf(&sb, "\n%s\n%s\n%s%s\n(summary omitted, already seen in previous search)%s\n\n", p.Title, p.Slug, p.PageType, aliasesTag, snippetTag) } else { - fmt.Fprintf(&sb, "\n%s\n%s\n%s\n%s%s\n\n", p.Title, p.Slug, p.PageType, p.Summary, snippetTag) + fmt.Fprintf(&sb, "\n%s\n%s\n%s%s\n%s%s\n\n", p.Title, p.Slug, p.PageType, aliasesTag, p.Summary, snippetTag) } } sb.WriteString("") @@ -327,6 +334,36 @@ func parseStringOrArray(val any) []string { return nil } +// resolveSourceRefs enriches plain knowledge UUIDs to "uuid|title" format. +// Refs already in "uuid|title" format are left unchanged. +func resolveSourceRefs(ctx context.Context, knowledgeService interfaces.KnowledgeService, refs []string) []string { + if len(refs) == 0 || knowledgeService == nil { + return refs + } + resolved := make([]string, 0, len(refs)) + for _, ref := range refs { + if strings.Contains(ref, "|") { + resolved = append(resolved, ref) + continue + } + kn, err := knowledgeService.GetKnowledgeByIDOnly(ctx, ref) + if err != nil || kn == nil { + resolved = append(resolved, ref) + continue + } + title := kn.Title + if title == "" { + title = kn.FileName + } + if title != "" { + resolved = append(resolved, ref+"|"+title) + } else { + resolved = append(resolved, ref) + } + } + return resolved +} + func extractSnippet(content string, query string) string { if content == "" || query == "" { return "" diff --git a/internal/agent/tools/wiki_write_page.go b/internal/agent/tools/wiki_write_page.go index 6cf805a30..40402cc4a 100644 --- a/internal/agent/tools/wiki_write_page.go +++ b/internal/agent/tools/wiki_write_page.go @@ -13,12 +13,13 @@ import ( type wikiWritePageTool struct { BaseTool - wikiPageService interfaces.WikiPageService - kbIDs []string + wikiPageService interfaces.WikiPageService + knowledgeService interfaces.KnowledgeService + kbIDs []string } // NewWikiWritePageTool creates a new wiki_write_page tool -func NewWikiWritePageTool(wikiPageService interfaces.WikiPageService, kbIDs []string) types.Tool { +func NewWikiWritePageTool(wikiPageService interfaces.WikiPageService, kbIDs []string, knowledgeService interfaces.KnowledgeService) types.Tool { return &wikiWritePageTool{ BaseTool: NewBaseTool( ToolWikiWritePage, @@ -54,14 +55,15 @@ func NewWikiWritePageTool(wikiPageService interfaces.WikiPageService, kbIDs []st "source_refs": { "type": "array", "items": {"type": "string"}, - "description": "A list of source knowledge IDs that contributed to this page. If provided, these will COMPLETELY REPLACE the existing source_refs of the page." + "description": "A list of source knowledge IDs (UUIDs only) that contributed to this page. If provided, these will COMPLETELY REPLACE the existing source_refs of the page." } }, "required": ["slug", "title", "summary", "content", "page_type"] }`), ), - wikiPageService: wikiPageService, - kbIDs: kbIDs, + wikiPageService: wikiPageService, + knowledgeService: knowledgeService, + kbIDs: kbIDs, } } @@ -95,6 +97,8 @@ func (t *wikiWritePageTool) Execute(ctx context.Context, args json.RawMessage) ( return &types.ToolResult{Success: false, Error: "Failed to check existing page: " + err.Error()}, nil } + resolvedRefs := resolveSourceRefs(ctx, t.knowledgeService, params.SourceRefs) + var action string if existingPage != nil { // Update @@ -104,8 +108,8 @@ func (t *wikiWritePageTool) Execute(ctx context.Context, args json.RawMessage) ( existingPage.PageType = params.PageType existingPage.Aliases = params.Aliases - if len(params.SourceRefs) > 0 { - existingPage.SourceRefs = params.SourceRefs + if len(resolvedRefs) > 0 { + existingPage.SourceRefs = resolvedRefs } _, err = t.wikiPageService.UpdatePage(ctx, existingPage) @@ -123,7 +127,7 @@ func (t *wikiWritePageTool) Execute(ctx context.Context, args json.RawMessage) ( Content: params.Content, PageType: params.PageType, Aliases: params.Aliases, - SourceRefs: params.SourceRefs, + SourceRefs: resolvedRefs, } _, err = t.wikiPageService.CreatePage(ctx, newPage) if err != nil { diff --git a/internal/application/service/agent_service.go b/internal/application/service/agent_service.go index a5da2e999..f1250ff10 100644 --- a/internal/application/service/agent_service.go +++ b/internal/application/service/agent_service.go @@ -461,9 +461,9 @@ func (s *agentService) registerTools( case tools.ToolWikiUpdateIssue: toolToRegister = tools.NewWikiUpdateIssueTool(s.wikiPageService, wikiKBIDs) case tools.ToolWikiWritePage: - toolToRegister = tools.NewWikiWritePageTool(s.wikiPageService, wikiKBIDs) + toolToRegister = tools.NewWikiWritePageTool(s.wikiPageService, wikiKBIDs, s.knowledgeService) case tools.ToolWikiReplaceText: - toolToRegister = tools.NewWikiReplaceTextTool(s.wikiPageService, wikiKBIDs) + toolToRegister = tools.NewWikiReplaceTextTool(s.wikiPageService, wikiKBIDs, s.knowledgeService) case tools.ToolWikiRenamePage: toolToRegister = tools.NewWikiRenamePageTool(s.wikiPageService, wikiKBIDs) case tools.ToolWikiDeletePage: