- Enhanced the KB search handler to utilize the KB API for executing search queries, improving search accuracy and performance. - Implemented authorization checks for collections in the search requests, ensuring only accessible collections are queried. - Updated the search request structure to include metadata filtering capabilities, allowing for more refined search results. - Refactored unit tests to validate new search functionalities, including threshold handling and collection initialization checks, ensuring robust test coverage. - Adjusted the Makefile to streamline test coverage reporting and updated GitHub Actions workflows to include Codecov integration for better visibility on test coverage metrics.
183 lines
4.3 KiB
Go
183 lines
4.3 KiB
Go
package kb
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/yaoapp/yao/agent/search/types"
|
|
"github.com/yaoapp/yao/kb"
|
|
kbapi "github.com/yaoapp/yao/kb/api"
|
|
)
|
|
|
|
// Handler implements KB search using the KB API
|
|
type Handler struct {
|
|
config *types.KBConfig // KB search configuration
|
|
}
|
|
|
|
// NewHandler creates a new KB search handler
|
|
func NewHandler(cfg *types.KBConfig) *Handler {
|
|
return &Handler{config: cfg}
|
|
}
|
|
|
|
// Type returns the search type this handler supports
|
|
func (h *Handler) Type() types.SearchType {
|
|
return types.SearchTypeKB
|
|
}
|
|
|
|
// Search executes vector search and optional graph association
|
|
func (h *Handler) Search(req *types.Request) (*types.Result, error) {
|
|
start := time.Now()
|
|
|
|
// Validate request
|
|
if req.Query == "" {
|
|
return &types.Result{
|
|
Type: types.SearchTypeKB,
|
|
Query: req.Query,
|
|
Source: req.Source,
|
|
Items: []*types.ResultItem{},
|
|
Total: 0,
|
|
Duration: time.Since(start).Milliseconds(),
|
|
Error: "query is required",
|
|
}, nil
|
|
}
|
|
|
|
// Check if KB API is available
|
|
if kb.API == nil {
|
|
return &types.Result{
|
|
Type: types.SearchTypeKB,
|
|
Query: req.Query,
|
|
Source: req.Source,
|
|
Items: []*types.ResultItem{},
|
|
Total: 0,
|
|
Duration: time.Since(start).Milliseconds(),
|
|
Error: "knowledge base not initialized",
|
|
}, nil
|
|
}
|
|
|
|
// Get collections from request or config
|
|
collections := req.Collections
|
|
if len(collections) == 0 && h.config != nil {
|
|
collections = h.config.Collections
|
|
}
|
|
|
|
// If no collections specified, return empty result
|
|
if len(collections) == 0 {
|
|
return &types.Result{
|
|
Type: types.SearchTypeKB,
|
|
Query: req.Query,
|
|
Source: req.Source,
|
|
Items: []*types.ResultItem{},
|
|
Total: 0,
|
|
Duration: time.Since(start).Milliseconds(),
|
|
}, nil
|
|
}
|
|
|
|
// Get threshold from request or config
|
|
threshold := req.Threshold
|
|
if threshold == 0 && h.config != nil && h.config.Threshold > 0 {
|
|
threshold = h.config.Threshold
|
|
}
|
|
if threshold == 0 {
|
|
threshold = 0.7 // default
|
|
}
|
|
|
|
// Get limit
|
|
limit := req.Limit
|
|
if limit == 0 {
|
|
limit = 10 // default
|
|
}
|
|
|
|
// Determine search mode
|
|
mode := kbapi.SearchModeVector
|
|
if req.Graph {
|
|
mode = kbapi.SearchModeExpand
|
|
}
|
|
if h.config != nil && h.config.Graph {
|
|
mode = kbapi.SearchModeExpand
|
|
}
|
|
|
|
// Build KB API queries - one per collection
|
|
var queries []kbapi.Query
|
|
for _, collectionID := range collections {
|
|
queries = append(queries, kbapi.Query{
|
|
CollectionID: collectionID,
|
|
Input: req.Query,
|
|
Mode: mode,
|
|
Threshold: threshold,
|
|
PageSize: limit,
|
|
Metadata: req.Metadata,
|
|
})
|
|
}
|
|
|
|
// Execute search using KB API
|
|
ctx := context.Background()
|
|
searchResult, err := kb.API.Search(ctx, queries)
|
|
if err != nil {
|
|
return &types.Result{
|
|
Type: types.SearchTypeKB,
|
|
Query: req.Query,
|
|
Source: req.Source,
|
|
Items: []*types.ResultItem{},
|
|
Total: 0,
|
|
Duration: time.Since(start).Milliseconds(),
|
|
Error: fmt.Sprintf("search failed: %v", err),
|
|
}, nil
|
|
}
|
|
|
|
// Convert segments to result items
|
|
// Note: MinScore filtering is already done by KB API, no need to filter again
|
|
items := make([]*types.ResultItem, 0, len(searchResult.Segments))
|
|
for _, seg := range searchResult.Segments {
|
|
item := &types.ResultItem{
|
|
Type: types.SearchTypeKB,
|
|
Source: req.Source,
|
|
Score: seg.Score,
|
|
Content: seg.Text,
|
|
DocumentID: seg.DocumentID,
|
|
Collection: seg.CollectionID,
|
|
Metadata: seg.Metadata,
|
|
}
|
|
|
|
// Extract title from metadata if available
|
|
if seg.Metadata != nil {
|
|
if title, ok := seg.Metadata["title"].(string); ok {
|
|
item.Title = title
|
|
}
|
|
}
|
|
|
|
items = append(items, item)
|
|
}
|
|
|
|
// Convert graph data if available
|
|
var graphNodes []*types.GraphNode
|
|
if searchResult.Graph != nil {
|
|
for _, node := range searchResult.Graph.Nodes {
|
|
// Extract name from properties if available
|
|
name := ""
|
|
if node.Properties != nil {
|
|
if n, ok := node.Properties["name"].(string); ok {
|
|
name = n
|
|
}
|
|
}
|
|
graphNodes = append(graphNodes, &types.GraphNode{
|
|
ID: node.ID,
|
|
Type: node.EntityType,
|
|
Name: name,
|
|
Metadata: node.Properties,
|
|
})
|
|
}
|
|
}
|
|
|
|
result := &types.Result{
|
|
Type: types.SearchTypeKB,
|
|
Query: req.Query,
|
|
Source: req.Source,
|
|
Items: items,
|
|
Total: len(items),
|
|
Duration: time.Since(start).Milliseconds(),
|
|
GraphNodes: graphNodes,
|
|
}
|
|
|
|
return result, nil
|
|
}
|