d546406a15
- Updated the formatKnowledgeBaseList function to return knowledge base information as XML, improving data structure for integration. - Modified the formatOutput function in GrepChunksTool to format search results as XML, enhancing readability and consistency. - Adjusted the formatOutput method in KnowledgeSearchTool to output search results in XML format, providing a structured response. - Refactored the buildOutput function in ListKnowledgeChunksTool to generate XML output, improving the clarity of chunk information. These changes standardize output formats across multiple tools, facilitating better data handling and integration.
323 lines
9.0 KiB
Go
323 lines
9.0 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/Tencent/WeKnora/internal/searchutil"
|
|
"github.com/Tencent/WeKnora/internal/types"
|
|
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
|
)
|
|
|
|
var listKnowledgeChunksTool = BaseTool{
|
|
name: ToolListKnowledgeChunks,
|
|
description: `Retrieve full chunk content for a document by knowledge_id.
|
|
|
|
## Use After grep_chunks or knowledge_search:
|
|
1. grep_chunks(["keyword", "variant"]) → get knowledge_id
|
|
2. list_knowledge_chunks(knowledge_id) → read full content
|
|
|
|
## When to Use:
|
|
- Need full content of chunks from a known document
|
|
- Want to see context around specific chunks
|
|
- Check how many chunks a document has
|
|
|
|
## Parameters:
|
|
- knowledge_id (required): Document ID
|
|
- limit (optional): Chunks per page (default 20, max 100)
|
|
- offset (optional): Start position (default 0)
|
|
|
|
## Output:
|
|
Full chunk content with chunk_id, chunk_index, and content text.`,
|
|
schema: json.RawMessage(`{
|
|
"type": "object",
|
|
"properties": {
|
|
"knowledge_id": {
|
|
"type": "string",
|
|
"description": "Document ID to retrieve chunks from"
|
|
},
|
|
"limit": {
|
|
"type": "integer",
|
|
"description": "Chunks per page (default 20, max 100)",
|
|
"default": 20,
|
|
"minimum": 1,
|
|
"maximum": 100
|
|
},
|
|
"offset": {
|
|
"type": "integer",
|
|
"description": "Start position (default 0)",
|
|
"default": 0,
|
|
"minimum": 0
|
|
}
|
|
},
|
|
"required": ["knowledge_id", "limit", "offset"]
|
|
}`),
|
|
}
|
|
|
|
// ListKnowledgeChunksInput defines the input parameters for list knowledge chunks tool
|
|
type ListKnowledgeChunksInput struct {
|
|
KnowledgeID string `json:"knowledge_id"`
|
|
Limit int `json:"limit"`
|
|
Offset int `json:"offset"`
|
|
}
|
|
|
|
// ListKnowledgeChunksTool retrieves chunk snapshots for a specific knowledge document.
|
|
type ListKnowledgeChunksTool struct {
|
|
BaseTool
|
|
chunkService interfaces.ChunkService
|
|
knowledgeService interfaces.KnowledgeService
|
|
searchTargets types.SearchTargets // Pre-computed unified search targets with KB-tenant mapping
|
|
}
|
|
|
|
// NewListKnowledgeChunksTool creates a new tool instance.
|
|
func NewListKnowledgeChunksTool(
|
|
knowledgeService interfaces.KnowledgeService,
|
|
chunkService interfaces.ChunkService,
|
|
searchTargets types.SearchTargets,
|
|
) *ListKnowledgeChunksTool {
|
|
return &ListKnowledgeChunksTool{
|
|
BaseTool: listKnowledgeChunksTool,
|
|
chunkService: chunkService,
|
|
knowledgeService: knowledgeService,
|
|
searchTargets: searchTargets,
|
|
}
|
|
}
|
|
|
|
// Execute performs the chunk fetch against the chunk service.
|
|
func (t *ListKnowledgeChunksTool) Execute(ctx context.Context, args json.RawMessage) (*types.ToolResult, error) {
|
|
// Parse args from json.RawMessage
|
|
var input ListKnowledgeChunksInput
|
|
if err := json.Unmarshal(args, &input); err != nil {
|
|
return &types.ToolResult{
|
|
Success: false,
|
|
Error: fmt.Sprintf("Failed to parse args: %v", err),
|
|
}, err
|
|
}
|
|
|
|
knowledgeID := input.KnowledgeID
|
|
ok := knowledgeID != ""
|
|
if !ok || strings.TrimSpace(knowledgeID) == "" {
|
|
return &types.ToolResult{
|
|
Success: false,
|
|
Error: "knowledge_id is required",
|
|
}, fmt.Errorf("knowledge_id is required")
|
|
}
|
|
knowledgeID = strings.TrimSpace(knowledgeID)
|
|
|
|
// Get knowledge info without tenant filter to support shared KB
|
|
knowledge, err := t.knowledgeService.GetKnowledgeByIDOnly(ctx, knowledgeID)
|
|
if err != nil {
|
|
return &types.ToolResult{
|
|
Success: false,
|
|
Error: fmt.Sprintf("Knowledge not found: %v", err),
|
|
}, err
|
|
}
|
|
|
|
// Verify the knowledge's KB is in searchTargets (permission check)
|
|
if !t.searchTargets.ContainsKB(knowledge.KnowledgeBaseID) {
|
|
return &types.ToolResult{
|
|
Success: false,
|
|
Error: fmt.Sprintf("Knowledge base %s is not accessible", knowledge.KnowledgeBaseID),
|
|
}, fmt.Errorf("knowledge base not in search targets")
|
|
}
|
|
|
|
// Use the knowledge's actual tenant_id for chunk query (supports cross-tenant shared KB)
|
|
effectiveTenantID := knowledge.TenantID
|
|
|
|
chunkLimit := 20
|
|
if input.Limit > 0 {
|
|
chunkLimit = input.Limit
|
|
}
|
|
offset := 0
|
|
if input.Offset > 0 {
|
|
offset = input.Offset
|
|
}
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
|
|
pagination := &types.Pagination{
|
|
Page: offset/chunkLimit + 1,
|
|
PageSize: chunkLimit,
|
|
}
|
|
|
|
chunks, total, err := t.chunkService.GetRepository().ListPagedChunksByKnowledgeID(ctx,
|
|
effectiveTenantID, knowledgeID, pagination, []types.ChunkType{types.ChunkTypeText, types.ChunkTypeFAQ}, "", "", "", "", "")
|
|
if err != nil {
|
|
return &types.ToolResult{
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to list chunks: %v", err),
|
|
}, err
|
|
}
|
|
if chunks == nil {
|
|
return &types.ToolResult{
|
|
Success: false,
|
|
Error: "chunk query returned no data",
|
|
}, fmt.Errorf("chunk query returned no data")
|
|
}
|
|
|
|
totalChunks := total
|
|
fetched := len(chunks)
|
|
|
|
// Enrich image info from child image chunks (lazy loading)
|
|
if fetched > 0 {
|
|
chunkIDs := make([]string, 0, fetched)
|
|
for _, c := range chunks {
|
|
chunkIDs = append(chunkIDs, c.ID)
|
|
}
|
|
infoMap := searchutil.CollectImageInfoByChunkIDs(ctx, t.chunkService.GetRepository(), effectiveTenantID, chunkIDs)
|
|
for _, c := range chunks {
|
|
if c.ImageInfo == "" {
|
|
if merged, ok := infoMap[c.ID]; ok {
|
|
c.ImageInfo = merged
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
knowledgeTitle := t.lookupKnowledgeTitle(ctx, knowledgeID)
|
|
|
|
output := t.buildOutput(knowledgeID, knowledgeTitle, totalChunks, fetched, chunks)
|
|
|
|
formattedChunks := make([]map[string]interface{}, 0, len(chunks))
|
|
for idx, c := range chunks {
|
|
chunkData := map[string]interface{}{
|
|
"seq": idx + 1,
|
|
"chunk_id": c.ID,
|
|
"chunk_index": c.ChunkIndex,
|
|
"content": c.Content,
|
|
"chunk_type": c.ChunkType,
|
|
"knowledge_id": c.KnowledgeID,
|
|
"knowledge_base": c.KnowledgeBaseID,
|
|
"start_at": c.StartAt,
|
|
"end_at": c.EndAt,
|
|
"parent_chunk_id": c.ParentChunkID,
|
|
}
|
|
|
|
// 添加图片信息
|
|
if c.ImageInfo != "" {
|
|
var imageInfos []types.ImageInfo
|
|
if err := json.Unmarshal([]byte(c.ImageInfo), &imageInfos); err == nil && len(imageInfos) > 0 {
|
|
imageList := make([]map[string]string, 0, len(imageInfos))
|
|
for _, img := range imageInfos {
|
|
imgData := make(map[string]string)
|
|
if img.URL != "" {
|
|
imgData["url"] = img.URL
|
|
}
|
|
if img.Caption != "" {
|
|
imgData["caption"] = img.Caption
|
|
}
|
|
if img.OCRText != "" {
|
|
imgData["ocr_text"] = img.OCRText
|
|
}
|
|
if len(imgData) > 0 {
|
|
imageList = append(imageList, imgData)
|
|
}
|
|
}
|
|
if len(imageList) > 0 {
|
|
chunkData["images"] = imageList
|
|
}
|
|
}
|
|
}
|
|
|
|
formattedChunks = append(formattedChunks, chunkData)
|
|
}
|
|
|
|
return &types.ToolResult{
|
|
Success: true,
|
|
Output: output,
|
|
Data: map[string]interface{}{
|
|
"knowledge_id": knowledgeID,
|
|
"knowledge_title": knowledgeTitle,
|
|
"total_chunks": totalChunks,
|
|
"fetched_chunks": fetched,
|
|
"page": pagination.Page,
|
|
"page_size": pagination.PageSize,
|
|
"chunks": formattedChunks,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// lookupKnowledgeTitle looks up the title of a knowledge document
|
|
// Uses GetKnowledgeByIDOnly to support cross-tenant shared KB
|
|
func (t *ListKnowledgeChunksTool) lookupKnowledgeTitle(ctx context.Context, knowledgeID string) string {
|
|
if t.knowledgeService == nil {
|
|
return ""
|
|
}
|
|
knowledge, err := t.knowledgeService.GetKnowledgeByIDOnly(ctx, knowledgeID)
|
|
if err != nil || knowledge == nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(knowledge.Title)
|
|
}
|
|
|
|
// buildOutput builds the output as XML for the list knowledge chunks tool
|
|
func (t *ListKnowledgeChunksTool) buildOutput(
|
|
knowledgeID string,
|
|
knowledgeTitle string,
|
|
total int64,
|
|
fetched int,
|
|
chunks []*types.Chunk,
|
|
) string {
|
|
var b strings.Builder
|
|
|
|
titleAttr := ""
|
|
if knowledgeTitle != "" {
|
|
titleAttr = fmt.Sprintf(" title=\"%s\"", knowledgeTitle)
|
|
}
|
|
fmt.Fprintf(&b, "<knowledge_chunks knowledge_id=\"%s\"%s total=\"%d\" fetched=\"%d\">\n",
|
|
knowledgeID, titleAttr, total, fetched)
|
|
|
|
if fetched == 0 {
|
|
b.WriteString("</knowledge_chunks>")
|
|
return b.String()
|
|
}
|
|
|
|
for _, c := range chunks {
|
|
fmt.Fprintf(&b, "<chunk chunk_id=\"%s\" chunk_index=\"%d\" type=\"%s\">\n",
|
|
c.ID, c.ChunkIndex, c.ChunkType)
|
|
fmt.Fprintf(&b, "<content>%s</content>\n", summarizeContent(c.Content))
|
|
|
|
if c.ImageInfo != "" {
|
|
var imageInfos []types.ImageInfo
|
|
if err := json.Unmarshal([]byte(c.ImageInfo), &imageInfos); err == nil && len(imageInfos) > 0 {
|
|
for _, img := range imageInfos {
|
|
if img.URL != "" {
|
|
fmt.Fprintf(&b, "<image url=\"%s\">\n", img.URL)
|
|
} else {
|
|
b.WriteString("<image>\n")
|
|
}
|
|
if img.Caption != "" {
|
|
fmt.Fprintf(&b, "<image_caption>%s</image_caption>\n", img.Caption)
|
|
}
|
|
if img.OCRText != "" {
|
|
fmt.Fprintf(&b, "<image_ocr>%s</image_ocr>\n", img.OCRText)
|
|
}
|
|
b.WriteString("</image>\n")
|
|
}
|
|
}
|
|
}
|
|
|
|
b.WriteString("</chunk>\n")
|
|
}
|
|
|
|
if int64(fetched) < total {
|
|
fmt.Fprintf(&b, "<pagination remaining=\"%d\" />\n", int64(total)-int64(fetched))
|
|
}
|
|
|
|
b.WriteString("</knowledge_chunks>")
|
|
return b.String()
|
|
}
|
|
|
|
// summarizeContent summarizes the content of a chunk
|
|
func summarizeContent(content string) string {
|
|
cleaned := strings.TrimSpace(content)
|
|
if cleaned == "" {
|
|
return "(empty)"
|
|
}
|
|
|
|
return strings.TrimSpace(string(cleaned))
|
|
}
|