feat: Enhance wiki tools and agent system prompt for improved knowledge retrieval

- Updated the agent system prompt to clarify the workflow for knowledge retrieval, emphasizing the use of `wiki_search` and `wiki_read_page` tools for specific queries and general overviews.
- Modified the `wikiReadPageTool` to support reading multiple wiki pages simultaneously, improving efficiency in fetching content.
- Enhanced the `wikiSearchTool` to utilize PostgreSQL POSIX regular expressions for more effective search queries, allowing for complex pattern matching.
- Adjusted the `wikiPageRepository` to replace `ILIKE` with regex matching for search queries, increasing the precision of search results.

These changes significantly improve the agent's ability to retrieve and synthesize information from the wiki, enhancing user interactions and response accuracy.
This commit is contained in:
wizardchen
2026-04-09 22:56:08 +08:00
parent ba552c9042
commit 76c13c9bfe
3 changed files with 177 additions and 87 deletions
@@ -244,9 +244,15 @@ templates:
<workflow>
Follow this "Search-Read-Expand" cycle:
1. **Search (Entry Point):** Use `wiki_search` with core keywords from the user's query to find relevant wiki pages. (Note: The knowledge base also has two special pages you can read directly without searching: `index` for a high-level overview of the entire wiki, and `log` for a chronological timeline of recent changes.)
2. **Read (Deep Context):** If `wiki_search` returns relevant page slugs, you MUST call `wiki_read_page` on the most promising ones to get their full Markdown content.
3. **Expand (Follow Links):** Wiki pages contain `[[slug]]` cross-references. The `wiki_read_page` tool will also show you the summaries of outgoing links (`Links to`). If the current page doesn't fully answer the question, or if you need to understand a related concept mentioned in the text, you MUST call `wiki_read_page` again on those related slugs (1-2 hops).
1. **Determine Entry Point:**
- For specific factual questions, use `wiki_search` with core keywords to find relevant page slugs.
- For general overviews of the entire knowledge base, directly call `wiki_read_page` with the slug `index`.
- For recent updates, history, or timelines, directly call `wiki_read_page` with the slug `log`.
2. **Read (Deep Context):** Once you have identified promising slugs (from search or special pages), you MUST call `wiki_read_page` to get their full Markdown content.
3. **Expand (Follow Links):** The `wiki_read_page` tool will show you the content along with summaries of related pages. You should use these to navigate the knowledge graph:
- **Outgoing links (`Links to`):** Use these to dive deeper into specific concepts, people, or items mentioned in the current page's text.
- **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. **Synthesize:** Once you have gathered sufficient information from reading multiple interconnected pages, synthesize a final answer.
</workflow>
@@ -259,17 +265,11 @@ templates:
</constraints>
<tool_guidelines>
* **wiki_search:** Use this first to find entry points. Use concise keywords (1-2 words).
* **wiki_read_page:** Your primary tool. Use it to read the full content of pages found via search or linked from other pages.
* **thinking:** Use to plan your traversal path (e.g., "I read Entity A, which mentions Concept B. I need to read Concept B next to answer the second part of the question").
* **todo_write:** Track multi-step research (e.g., comparing three different entities).
* **wiki_search:** Use this to find entry points. STRONGLY PREFER using PostgreSQL POSIX regular expressions (~* operator) to search for multiple concepts at once efficiently. Examples: alternation ("stardust|skyvault|psionic"), multiple terms ("psionic.*engine"), prefix ("^entity/.*"). Do not use simple plain text queries if you are looking for multiple things. You can provide multiple queries at once to search in parallel.
* **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.
</tool_guidelines>
<system_status>
Current Time: {{current_time}}
User Language: {{language}}
</system_status>
<user_selected_knowledge_bases>
{{knowledge_bases}}
</user_selected_knowledge_bases>
+164 -72
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"strings"
"sync"
"github.com/Tencent/WeKnora/internal/types"
"github.com/Tencent/WeKnora/internal/types/interfaces"
@@ -16,80 +17,102 @@ type wikiReadPageTool struct {
BaseTool
wikiService interfaces.WikiPageService
kbIDs []string
seenLinks map[string]bool
mu sync.Mutex
}
func NewWikiReadPageTool(wikiService interfaces.WikiPageService, kbIDs []string) types.Tool {
return &wikiReadPageTool{
BaseTool: NewBaseTool(
ToolWikiReadPage,
`Read a wiki page by its slug. Returns the full markdown content, metadata, and links.
Use this to read specific wiki pages when you know their slug (e.g. "entity/acme-corp", "concept/rag", "summary/document-title").`,
`Read one or more wiki pages by their slugs. Returns the full markdown content, metadata, and links.
Use this to read specific wiki pages when you know their slug (e.g. "entity/acme-corp", "concept/rag").`,
json.RawMessage(`{
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "The wiki page slug to read (e.g. 'entity/acme-corp', 'concept/rag')"
"slugs": {
"type": "array",
"items": { "type": "string" },
"description": "List of wiki page slugs to read (e.g. ['entity/acme-corp', 'index'])"
},
"knowledge_base_id": {
"type": "string",
"description": "Optional: specific knowledge base ID. If omitted, searches all wiki KBs."
}
},
"required": ["slug"]
"required": ["slugs"]
}`),
),
wikiService: wikiService,
kbIDs: kbIDs,
seenLinks: make(map[string]bool),
}
}
func (t *wikiReadPageTool) Execute(ctx context.Context, args json.RawMessage) (*types.ToolResult, error) {
var params struct {
Slug string `json:"slug"`
Slug any `json:"slug"`
Slugs any `json:"slugs"`
KnowledgeBaseID string `json:"knowledge_base_id"`
}
if err := json.Unmarshal(args, &params); err != nil {
return &types.ToolResult{Success: false, Error: "Invalid parameters: " + err.Error()}, nil
}
var slugsToFetch []string
slugsToFetch = append(slugsToFetch, parseStringOrArray(params.Slugs)...)
slugsToFetch = append(slugsToFetch, parseStringOrArray(params.Slug)...)
if len(slugsToFetch) == 0 {
return &types.ToolResult{Success: false, Error: "Missing 'slugs' parameter"}, nil
}
kbIDs := t.kbIDs
if params.KnowledgeBaseID != "" {
kbIDs = []string{params.KnowledgeBaseID}
}
for _, kbID := range kbIDs {
page, err := t.wikiService.GetPageBySlug(ctx, kbID, params.Slug)
if err == nil && page != nil {
// Resolve OutLinks summaries to provide 1-hop context
var outLinksDesc []string
if len(page.OutLinks) > 0 {
for _, outSlug := range page.OutLinks {
if linkPage, err := t.wikiService.GetPageBySlug(ctx, kbID, outSlug); err == nil && linkPage != nil {
outLinksDesc = append(outLinksDesc, fmt.Sprintf("[[%s]] (%s)", outSlug, linkPage.Summary))
} else {
outLinksDesc = append(outLinksDesc, fmt.Sprintf("[[%s]]", outSlug))
}
}
} else {
outLinksDesc = []string{"(none)"}
}
var outputs []string
var errs []string
// Resolve InLinks summaries to provide reverse 1-hop context
var inLinksDesc []string
if len(page.InLinks) > 0 {
for _, inSlug := range page.InLinks {
if linkPage, err := t.wikiService.GetPageBySlug(ctx, kbID, inSlug); err == nil && linkPage != nil {
inLinksDesc = append(inLinksDesc, fmt.Sprintf("[[%s]] (%s)", inSlug, linkPage.Summary))
} else {
inLinksDesc = append(inLinksDesc, fmt.Sprintf("[[%s]]", inSlug))
}
}
} else {
inLinksDesc = []string{"(none)"}
}
formatLinks := func(slugs []string, kbID string) []string {
var descs []string
for _, s := range slugs {
t.mu.Lock()
seen := t.seenLinks[s]
t.seenLinks[s] = true
t.mu.Unlock()
output := fmt.Sprintf(`<wiki_page>
if seen {
// We already injected the summary for this link in this session
descs = append(descs, fmt.Sprintf("[[%s]] (summary omitted, already seen)", s))
} else {
if linkPage, err := t.wikiService.GetPageBySlug(ctx, kbID, s); err == nil && linkPage != nil {
descs = append(descs, fmt.Sprintf("[[%s]] (%s)", s, linkPage.Summary))
} else {
descs = append(descs, fmt.Sprintf("[[%s]]", s))
}
}
}
if len(descs) == 0 {
return []string{"(none)"}
}
return descs
}
for _, slug := range slugsToFetch {
found := false
for _, kbID := range kbIDs {
page, err := t.wikiService.GetPageBySlug(ctx, kbID, slug)
if err == nil && page != nil {
t.mu.Lock()
t.seenLinks[slug] = true
t.mu.Unlock()
outLinksDesc := formatLinks(page.OutLinks, kbID)
inLinksDesc := formatLinks(page.InLinks, kbID)
output := fmt.Sprintf(`<wiki_page>
<metadata>
<title>%s</title>
<slug>%s</slug>
@@ -103,16 +126,31 @@ func (t *wikiReadPageTool) Execute(ctx context.Context, args json.RawMessage) (*
%s
</content>
</wiki_page>`,
page.Title, page.Slug, page.PageType,
strings.Join(outLinksDesc, ", "),
strings.Join(inLinksDesc, ", "),
page.Content,
)
return &types.ToolResult{Success: true, Output: output}, nil
page.Title, page.Slug, page.PageType,
strings.Join(outLinksDesc, ", "),
strings.Join(inLinksDesc, ", "),
page.Content,
)
outputs = append(outputs, output)
found = true
break
}
}
if !found {
errs = append(errs, fmt.Sprintf("Wiki page '%s' not found", slug))
}
}
return &types.ToolResult{Success: false, Error: fmt.Sprintf("Wiki page '%s' not found", params.Slug)}, nil
if len(outputs) == 0 {
return &types.ToolResult{Success: false, Error: strings.Join(errs, "; ")}, nil
}
finalOutput := strings.Join(outputs, "\n\n")
if len(errs) > 0 {
finalOutput += fmt.Sprintf("\n\n<errors>\n%s\n</errors>", strings.Join(errs, "\n"))
}
return &types.ToolResult{Success: true, Output: finalOutput}, nil
}
// ---- wiki_search ----
@@ -121,69 +159,102 @@ type wikiSearchTool struct {
BaseTool
wikiService interfaces.WikiPageService
kbIDs []string
seenSlugs map[string]bool
mu sync.Mutex
}
func NewWikiSearchTool(wikiService interfaces.WikiPageService, kbIDs []string) types.Tool {
return &wikiSearchTool{
BaseTool: NewBaseTool(
ToolWikiSearch,
`Search wiki pages by keyword. Returns matching pages with titles, slugs, and summaries.
`Search wiki pages using PostgreSQL POSIX regular expressions (~* operator, case-insensitive).
STRONGLY PREFER using regex to search for multiple concepts at once rather than simple plain text queries.
Returns matching pages with titles, slugs, and summaries.
Examples:
- Alternation (RECOMMENDED): "stardust|skyvault" (matches either word)
- Multiple terms (RECOMMENDED): "psionic.*engine" (matches both words in order)
- Prefix matching: "^entity/.*" (finds all entities)
- Plain text: "engine" (matches anywhere in title/content/slug/summary)
Use this to find relevant wiki pages when you don't know the exact slug.`,
json.RawMessage(`{
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query (keywords or phrase)"
"queries": {
"type": "array",
"items": { "type": "string" },
"description": "List of regex search queries to run"
},
"limit": {
"type": "integer",
"description": "Max results to return (default 10)"
"description": "Max results to return per query (default 10)"
}
},
"required": ["query"]
"required": ["queries"]
}`),
),
wikiService: wikiService,
kbIDs: kbIDs,
seenSlugs: make(map[string]bool),
}
}
func (t *wikiSearchTool) Execute(ctx context.Context, args json.RawMessage) (*types.ToolResult, error) {
var params struct {
Query string `json:"query"`
Limit int `json:"limit"`
Query any `json:"query"`
Queries any `json:"queries"`
Limit int `json:"limit"`
}
if err := json.Unmarshal(args, &params); err != nil {
return &types.ToolResult{Success: false, Error: "Invalid parameters: " + err.Error()}, nil
}
var queriesToRun []string
queriesToRun = append(queriesToRun, parseStringOrArray(params.Queries)...)
queriesToRun = append(queriesToRun, parseStringOrArray(params.Query)...)
if len(queriesToRun) == 0 {
return &types.ToolResult{Success: false, Error: "Missing 'queries' parameter"}, nil
}
if params.Limit <= 0 {
params.Limit = 10
}
var allPages []*types.WikiPage
for _, kbID := range t.kbIDs {
pages, err := t.wikiService.SearchPages(ctx, kbID, params.Query, params.Limit)
if err == nil {
allPages = append(allPages, pages...)
var allOutputs []string
for _, query := range queriesToRun {
var allPages []*types.WikiPage
for _, kbID := range t.kbIDs {
pages, err := t.wikiService.SearchPages(ctx, kbID, query, params.Limit)
if err == nil {
allPages = append(allPages, pages...)
}
}
if len(allPages) == 0 {
allOutputs = append(allOutputs, fmt.Sprintf("<search_results count=\"0\" query=\"%s\" />", query))
continue
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("<search_results count=\"%d\" query=\"%s\">\n", len(allPages), query))
for _, p := range allPages {
t.mu.Lock()
seen := t.seenSlugs[p.Slug]
t.seenSlugs[p.Slug] = true
t.mu.Unlock()
if seen {
fmt.Fprintf(&sb, "<page>\n<title>%s</title>\n<slug>%s</slug>\n<type>%s</type>\n<summary>(summary omitted, already seen in previous search)</summary>\n</page>\n", p.Title, p.Slug, p.PageType)
} else {
fmt.Fprintf(&sb, "<page>\n<title>%s</title>\n<slug>%s</slug>\n<type>%s</type>\n<summary>%s</summary>\n</page>\n", p.Title, p.Slug, p.PageType, p.Summary)
}
}
sb.WriteString("</search_results>")
allOutputs = append(allOutputs, sb.String())
}
if len(allPages) == 0 {
return &types.ToolResult{
Success: true,
Output: fmt.Sprintf("<search_results count=\"0\" query=\"%s\" />", params.Query),
}, nil
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("<search_results count=\"%d\" query=\"%s\">\n", len(allPages), params.Query))
for _, p := range allPages {
fmt.Fprintf(&sb, "<page>\n<title>%s</title>\n<slug>%s</slug>\n<type>%s</type>\n<summary>%s</summary>\n</page>\n", p.Title, p.Slug, p.PageType, p.Summary)
}
sb.WriteString("</search_results>")
return &types.ToolResult{Success: true, Output: sb.String()}, nil
return &types.ToolResult{Success: true, Output: strings.Join(allOutputs, "\n\n")}, nil
}
// --- Helper ---
@@ -200,3 +271,24 @@ func truncateForSummary(content string, maxLen int) string {
}
return summary
}
func parseStringOrArray(val any) []string {
if val == nil {
return nil
}
switch v := val.(type) {
case string:
if v != "" {
return []string{v}
}
case []interface{}:
var res []string
for _, item := range v {
if s, ok := item.(string); ok && s != "" {
res = append(res, s)
}
}
return res
}
return nil
}
+2 -4
View File
@@ -242,12 +242,10 @@ func (r *wikiPageRepository) Search(ctx context.Context, kbID string, query stri
limit = 50
}
likePattern := "%" + query + "%"
var pages []*types.WikiPage
if err := r.db.WithContext(ctx).
Where("knowledge_base_id = ? AND (title ILIKE ? OR content ILIKE ? OR summary ILIKE ? OR slug ILIKE ?)",
kbID, likePattern, likePattern, likePattern, likePattern).
Where("knowledge_base_id = ? AND (title ~* ? OR content ~* ? OR summary ~* ? OR slug ~* ?)",
kbID, query, query, query, query).
Where("status != ?", "archived").
Order("updated_at DESC").
Limit(limit).