yao/agent/search/jsapi.go
Max 7768ca73b3 Enhance Search API with New V8 Binding Methods
- Introduced a new search object in the context to expose search methods (Web, KB, DB, All, Any, Race) for JavaScript integration.
- Implemented individual search methods with argument validation and error handling, improving the robustness of the API.
- Updated the JSAPI implementation to utilize the new search object, ensuring seamless interaction with the search functionalities.
- Enhanced documentation in DESIGN.md to reflect the new V8 binding methods and their usage, providing clear guidance for developers.
2025-12-13 16:11:30 +08:00

229 lines
6.8 KiB
Go

package search
import (
"github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/search/types"
)
// JSAPI implements context.SearchAPI interface
// Provides ctx.search.Web(), ctx.search.KB(), ctx.search.DB(), ctx.search.All(), ctx.search.Any(), ctx.search.Race()
type JSAPI struct {
ctx *context.Context
searcher *Searcher
}
// NewJSAPI creates a new search JSAPI instance
func NewJSAPI(ctx *context.Context, config *types.Config, uses *Uses) *JSAPI {
return &JSAPI{
ctx: ctx,
searcher: New(config, uses),
}
}
// Web executes web search
// Options:
// - limit: int - max results (default: 10)
// - sites: []string - restrict to specific sites
// - time_range: string - "day", "week", "month", "year"
// - rerank: map[string]interface{} - rerank options
func (api *JSAPI) Web(query string, opts map[string]interface{}) interface{} {
req := api.buildRequest(types.SearchTypeWeb, query, opts)
result, _ := api.searcher.Search(api.ctx, req)
return result
}
// KB executes knowledge base search
// Options:
// - collections: []string - collection IDs
// - threshold: float64 - similarity threshold (0-1)
// - limit: int - max results
// - graph: bool - enable graph association
// - rerank: map[string]interface{} - rerank options
func (api *JSAPI) KB(query string, opts map[string]interface{}) interface{} {
req := api.buildRequest(types.SearchTypeKB, query, opts)
result, _ := api.searcher.Search(api.ctx, req)
return result
}
// DB executes database search
// Options:
// - models: []string - model IDs
// - wheres: []map[string]interface{} - pre-defined filters (GOU QueryDSL Where format)
// - orders: []map[string]interface{} - sort orders (GOU QueryDSL Order format)
// - select: []string - fields to return
// - limit: int - max results
// - rerank: map[string]interface{} - rerank options
func (api *JSAPI) DB(query string, opts map[string]interface{}) interface{} {
req := api.buildRequest(types.SearchTypeDB, query, opts)
result, _ := api.searcher.Search(api.ctx, req)
return result
}
// All executes all searches and waits for all to complete (like Promise.all)
// Each request should have:
// - type: string - "web", "kb", or "db"
// - query: string - search query
// - ... other type-specific options
func (api *JSAPI) All(requests []interface{}) []interface{} {
reqs := api.parseRequests(requests)
results, _ := api.searcher.All(api.ctx, reqs)
return api.convertResults(results)
}
// Any returns as soon as any search succeeds with results (like Promise.any)
// Each request should have:
// - type: string - "web", "kb", or "db"
// - query: string - search query
// - ... other type-specific options
func (api *JSAPI) Any(requests []interface{}) []interface{} {
reqs := api.parseRequests(requests)
results, _ := api.searcher.Any(api.ctx, reqs)
return api.convertResults(results)
}
// Race returns as soon as any search completes (like Promise.race)
// Each request should have:
// - type: string - "web", "kb", or "db"
// - query: string - search query
// - ... other type-specific options
func (api *JSAPI) Race(requests []interface{}) []interface{} {
reqs := api.parseRequests(requests)
results, _ := api.searcher.Race(api.ctx, reqs)
return api.convertResults(results)
}
// buildRequest builds a Request from query and options
func (api *JSAPI) buildRequest(searchType types.SearchType, query string, opts map[string]interface{}) *types.Request {
req := &types.Request{
Type: searchType,
Query: query,
Source: types.SourceHook, // JSAPI calls are from hooks
}
if opts == nil {
return req
}
// Common options
if limit, ok := opts["limit"].(float64); ok {
req.Limit = int(limit)
} else if limit, ok := opts["limit"].(int); ok {
req.Limit = limit
}
// Web-specific options
if searchType == types.SearchTypeWeb {
if sites, ok := opts["sites"].([]interface{}); ok {
req.Sites = toStringSlice(sites)
}
if timeRange, ok := opts["time_range"].(string); ok {
req.TimeRange = timeRange
}
}
// KB-specific options
if searchType == types.SearchTypeKB {
if collections, ok := opts["collections"].([]interface{}); ok {
req.Collections = toStringSlice(collections)
}
if threshold, ok := opts["threshold"].(float64); ok {
req.Threshold = threshold
}
if graph, ok := opts["graph"].(bool); ok {
req.Graph = graph
}
}
// DB-specific options
if searchType == types.SearchTypeDB {
if models, ok := opts["models"].([]interface{}); ok {
req.Models = toStringSlice(models)
}
if selectFields, ok := opts["select"].([]interface{}); ok {
req.Select = toStringSlice(selectFields)
}
// Note: wheres and orders are more complex, handled by QueryDSL generator
}
// Rerank options
if rerankOpts, ok := opts["rerank"].(map[string]interface{}); ok {
req.Rerank = &types.RerankOptions{}
if topN, ok := rerankOpts["top_n"].(float64); ok {
req.Rerank.TopN = int(topN)
} else if topN, ok := rerankOpts["top_n"].(int); ok {
req.Rerank.TopN = topN
}
}
return req
}
// parseRequests parses an array of request objects into typed Requests
func (api *JSAPI) parseRequests(requests []interface{}) []*types.Request {
reqs := make([]*types.Request, 0, len(requests))
for _, r := range requests {
reqMap, ok := r.(map[string]interface{})
if !ok {
continue
}
// Get type
typeStr, ok := reqMap["type"].(string)
if !ok {
continue
}
searchType := types.SearchType(typeStr)
// Get query
query, ok := reqMap["query"].(string)
if !ok {
continue
}
// Build request with remaining options
req := api.buildRequest(searchType, query, reqMap)
reqs = append(reqs, req)
}
return reqs
}
// convertResults converts typed Results to interface slice for JS
func (api *JSAPI) convertResults(results []*types.Result) []interface{} {
out := make([]interface{}, len(results))
for i, r := range results {
out[i] = r
}
return out
}
// toStringSlice converts []interface{} to []string
func toStringSlice(arr []interface{}) []string {
result := make([]string, 0, len(arr))
for _, v := range arr {
if s, ok := v.(string); ok {
result = append(result, s)
}
}
return result
}
// ConfigGetter is a function type that retrieves search config and uses for an assistant
type ConfigGetter func(assistantID string) (*types.Config, *Uses)
// configGetter is set by assistant package during initialization
var configGetter ConfigGetter
// SetJSAPIFactory sets the factory function for creating SearchAPI instances
// Called by assistant package during initialization
// getter: function to get search config and uses from assistant ID
func SetJSAPIFactory(getter ConfigGetter) {
configGetter = getter
context.SearchAPIFactory = func(ctx *context.Context) context.SearchAPI {
var config *types.Config
var uses *Uses
if configGetter != nil && ctx.AssistantID != "" {
config, uses = configGetter(ctx.AssistantID)
}
return NewJSAPI(ctx, config, uses)
}
}