- 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.
229 lines
6.8 KiB
Go
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)
|
|
}
|
|
}
|