package tools import ( "context" "fmt" "sync" "github.com/Tencent/WeKnora/internal/types" "github.com/Tencent/WeKnora/internal/types/interfaces" ) // GetDocumentInfoTool retrieves detailed information about a document/knowledge type GetDocumentInfoTool struct { BaseTool tenantID uint64 knowledgeService interfaces.KnowledgeService chunkService interfaces.ChunkService } // NewGetDocumentInfoTool creates a new get document info tool func NewGetDocumentInfoTool( tenantID uint64, knowledgeService interfaces.KnowledgeService, chunkService interfaces.ChunkService, ) *GetDocumentInfoTool { description := `Retrieve detailed metadata information about documents. ## When to Use Use this tool when: - Need to understand document basic information (title, type, size, etc.) - Check if document exists and is available - Batch query metadata for multiple documents - Understand document processing status Do not use when: - Need document content (use knowledge_search) - Need specific text chunks (search results already contain full content) ## Returned Information - Basic info: title, description, source type - File info: filename, type, size - Processing status: whether processed, chunk count - Metadata: custom tags and properties ## Notes - Concurrent query for multiple documents provides better performance - Returns complete document metadata, not just title - Can check document processing status (parse_status)` return &GetDocumentInfoTool{ BaseTool: NewBaseTool("get_document_info", description), tenantID: tenantID, knowledgeService: knowledgeService, chunkService: chunkService, } } // Parameters returns the JSON schema for the tool's parameters func (t *GetDocumentInfoTool) Parameters() map[string]interface{} { return map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "knowledge_ids": map[string]interface{}{ "type": "array", "description": "Array of document/knowledge IDs, obtained from knowledge_id field in search results, supports concurrent batch queries", "items": map[string]interface{}{ "type": "string", }, "minItems": 1, "maxItems": 10, }, }, "required": []string{"knowledge_ids"}, } } // Execute retrieves document information with concurrent processing func (t *GetDocumentInfoTool) Execute(ctx context.Context, args map[string]interface{}) (*types.ToolResult, error) { // Extract knowledge_ids array knowledgeIDsRaw, ok := args["knowledge_ids"].([]interface{}) if !ok || len(knowledgeIDsRaw) == 0 { return &types.ToolResult{ Success: false, Error: "knowledge_ids is required and must be a non-empty array", }, fmt.Errorf("knowledge_ids is required") } // Convert to string slice var knowledgeIDs []string for _, id := range knowledgeIDsRaw { if idStr, ok := id.(string); ok && idStr != "" { knowledgeIDs = append(knowledgeIDs, idStr) } } // Check if knowledge IDs are valid if len(knowledgeIDs) == 0 { return &types.ToolResult{ Success: false, Error: "knowledge_ids must contain at least one valid knowledge ID", }, fmt.Errorf("no valid knowledge IDs provided") } // Concurrently get info for each knowledge ID type docInfo struct { knowledge *types.Knowledge chunkCount int err error } var wg sync.WaitGroup var mu sync.Mutex results := make(map[string]*docInfo) // Concurrently get info for each knowledge ID for _, knowledgeID := range knowledgeIDs { wg.Add(1) go func(id string) { defer wg.Done() // Get knowledge metadata knowledge, err := t.knowledgeService.GetRepository().GetKnowledgeByID(ctx, t.tenantID, id) if err != nil { mu.Lock() results[id] = &docInfo{ err: fmt.Errorf("无法获取文档信息: %v", err), } mu.Unlock() return } // Get chunk count _, total, err := t.chunkService.GetRepository(). ListPagedChunksByKnowledgeID(ctx, t.tenantID, id, &types.Pagination{ Page: 1, PageSize: 1000, }, []types.ChunkType{"text"}, "", "") if err != nil { mu.Lock() results[id] = &docInfo{ err: fmt.Errorf("无法获取文档信息: %v", err), } mu.Unlock() return } chunkCount := int(total) mu.Lock() results[id] = &docInfo{ knowledge: knowledge, chunkCount: chunkCount, } mu.Unlock() }(knowledgeID) } wg.Wait() // Collect successful results and errors successDocs := make([]*docInfo, 0) var errors []string for _, knowledgeID := range knowledgeIDs { result := results[knowledgeID] if result.err != nil { errors = append(errors, fmt.Sprintf("%s: %v", knowledgeID, result.err)) } else if result.knowledge != nil { successDocs = append(successDocs, result) } } if len(successDocs) == 0 { return &types.ToolResult{ Success: false, Error: fmt.Sprintf("无法获取任何文档信息。错误: %v", errors), }, fmt.Errorf("all document retrievals failed") } // Format output output := "=== 文档信息 ===\n\n" output += fmt.Sprintf("成功获取 %d / %d 个文档信息\n\n", len(successDocs), len(knowledgeIDs)) if len(errors) > 0 { output += "=== 部分失败 ===\n" for _, errMsg := range errors { output += fmt.Sprintf(" - %s\n", errMsg) } output += "\n" } formattedDocs := make([]map[string]interface{}, 0, len(successDocs)) for i, doc := range successDocs { k := doc.knowledge output += fmt.Sprintf("【文档 #%d】\n", i+1) output += fmt.Sprintf(" ID: %s\n", k.ID) output += fmt.Sprintf(" 标题: %s\n", k.Title) if k.Description != "" { output += fmt.Sprintf(" 描述: %s\n", k.Description) } output += fmt.Sprintf(" 来源: %s\n", formatSource(k.Type, k.Source)) if k.FileName != "" { output += fmt.Sprintf(" 文件名: %s\n", k.FileName) output += fmt.Sprintf(" 文件类型: %s\n", k.FileType) output += fmt.Sprintf(" 文件大小: %s\n", formatFileSize(k.FileSize)) } output += fmt.Sprintf(" 处理状态: %s\n", formatParseStatus(k.ParseStatus)) output += fmt.Sprintf(" 分块数量: %d 个\n", doc.chunkCount) if k.Metadata != nil { if metadata, err := k.Metadata.Map(); err == nil && len(metadata) > 0 { output += " 元数据:\n" for key, value := range metadata { output += fmt.Sprintf(" - %s: %v\n", key, value) } } } output += "\n" formattedDocs = append(formattedDocs, map[string]interface{}{ "knowledge_id": k.ID, "title": k.Title, "description": k.Description, "type": k.Type, "source": k.Source, "file_name": k.FileName, "file_type": k.FileType, "file_size": k.FileSize, "parse_status": k.ParseStatus, "chunk_count": doc.chunkCount, "metadata": k.GetMetadata(), }) } // Extract first document title for summary var firstTitle string if len(successDocs) > 0 && successDocs[0].knowledge != nil { firstTitle = successDocs[0].knowledge.Title } return &types.ToolResult{ Success: true, Output: output, Data: map[string]interface{}{ "documents": formattedDocs, "total_docs": len(successDocs), "requested": len(knowledgeIDs), "errors": errors, "display_type": "document_info", "title": firstTitle, // For frontend summary display }, }, nil } func formatSource(knowledgeType, source string) string { switch knowledgeType { case "file": return "文件上传" case "url": return fmt.Sprintf("URL: %s", source) case "passage": return "文本输入" default: return knowledgeType } } func formatFileSize(size int64) string { if size == 0 { return "未知" } const unit = 1024 if size < unit { return fmt.Sprintf("%d B", size) } div, exp := int64(unit), 0 for n := size / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %cB", float64(size)/float64(div), "KMGTPE"[exp]) } func formatParseStatus(status string) string { switch status { case "pending": return "⏳ 待处理" case "processing": return "🔄 处理中" case "completed", "success": return "✅ 已完成" case "failed": return "❌ 失败" default: return status } }