yao/agent/context/jsapi_search.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

362 lines
11 KiB
Go

package context
import (
"github.com/yaoapp/gou/runtime/v8/bridge"
"rogchap.com/v8go"
)
// SearchAPI defines the search JSAPI interface for ctx.search.*
// This interface is defined here to avoid circular dependency between context and search packages.
// The actual implementation is in agent/search/jsapi.go
type SearchAPI interface {
// Web executes web search
// Returns *types.Result or error information
Web(query string, opts map[string]interface{}) interface{}
// KB executes knowledge base search
// Returns *types.Result or error information
KB(query string, opts map[string]interface{}) interface{}
// DB executes database search
// Returns *types.Result or error information
DB(query string, opts map[string]interface{}) interface{}
// Parallel search methods - inspired by JavaScript Promise
// All waits for all searches to complete (like Promise.all)
All(requests []interface{}) []interface{}
// Any returns when any search succeeds with results (like Promise.any)
Any(requests []interface{}) []interface{}
// Race returns when any search completes (like Promise.race)
Race(requests []interface{}) []interface{}
}
// SearchAPIFactory is a function type that creates a SearchAPI for a context
// This is set by the search package during initialization
var SearchAPIFactory func(ctx *Context) SearchAPI
// Search returns the search API for this context
// Returns nil if SearchAPIFactory is not set
func (ctx *Context) Search() SearchAPI {
if SearchAPIFactory == nil {
return nil
}
return SearchAPIFactory(ctx)
}
// newSearchObject creates a new search object with all search methods
// This is called from jsapi.go NewObject() to mount ctx.search
func (ctx *Context) newSearchObject(iso *v8go.Isolate) *v8go.ObjectTemplate {
searchObj := v8go.NewObjectTemplate(iso)
// Single search methods
searchObj.Set("Web", ctx.searchWebMethod(iso))
searchObj.Set("KB", ctx.searchKBMethod(iso))
searchObj.Set("DB", ctx.searchDBMethod(iso))
// Parallel search methods - inspired by JavaScript Promise
searchObj.Set("All", ctx.searchAllMethod(iso))
searchObj.Set("Any", ctx.searchAnyMethod(iso))
searchObj.Set("Race", ctx.searchRaceMethod(iso))
return searchObj
}
// searchWebMethod implements ctx.search.Web(query, options?)
// Options:
// - limit: number - max results (default: 10)
// - sites: string[] - restrict to specific sites
// - time_range: string - "day", "week", "month", "year"
// - rerank: { top_n: number } - rerank options
func (ctx *Context) searchWebMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
v8ctx := info.Context()
args := info.Args()
// Validate arguments
if len(args) < 1 {
return bridge.JsException(v8ctx, "Web requires query parameter")
}
// Get query string
if !args[0].IsString() {
return bridge.JsException(v8ctx, "query must be a string")
}
query := args[0].String()
// Parse options (optional)
var opts map[string]interface{}
if len(args) >= 2 && !args[1].IsUndefined() && !args[1].IsNull() {
goVal, err := bridge.GoValue(args[1], v8ctx)
if err != nil {
return bridge.JsException(v8ctx, "invalid options: "+err.Error())
}
if optsMap, ok := goVal.(map[string]interface{}); ok {
opts = optsMap
}
}
// Get search API
searchAPI := ctx.Search()
if searchAPI == nil {
return bridge.JsException(v8ctx, "search API not available")
}
// Execute search
result := searchAPI.Web(query, opts)
// Convert result to JS value
jsVal, err := bridge.JsValue(v8ctx, result)
if err != nil {
return bridge.JsException(v8ctx, "failed to convert result: "+err.Error())
}
return jsVal
})
}
// searchKBMethod implements ctx.search.KB(query, options?)
// Options:
// - collections: string[] - collection IDs
// - threshold: number - similarity threshold (0-1)
// - limit: number - max results
// - graph: boolean - enable graph association
// - rerank: { top_n: number } - rerank options
func (ctx *Context) searchKBMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
v8ctx := info.Context()
args := info.Args()
// Validate arguments
if len(args) < 1 {
return bridge.JsException(v8ctx, "KB requires query parameter")
}
// Get query string
if !args[0].IsString() {
return bridge.JsException(v8ctx, "query must be a string")
}
query := args[0].String()
// Parse options (optional)
var opts map[string]interface{}
if len(args) >= 2 && !args[1].IsUndefined() && !args[1].IsNull() {
goVal, err := bridge.GoValue(args[1], v8ctx)
if err != nil {
return bridge.JsException(v8ctx, "invalid options: "+err.Error())
}
if optsMap, ok := goVal.(map[string]interface{}); ok {
opts = optsMap
}
}
// Get search API
searchAPI := ctx.Search()
if searchAPI == nil {
return bridge.JsException(v8ctx, "search API not available")
}
// Execute search
result := searchAPI.KB(query, opts)
// Convert result to JS value
jsVal, err := bridge.JsValue(v8ctx, result)
if err != nil {
return bridge.JsException(v8ctx, "failed to convert result: "+err.Error())
}
return jsVal
})
}
// searchDBMethod implements ctx.search.DB(query, options?)
// Options:
// - models: string[] - model IDs
// - wheres: Where[] - pre-defined filters (GOU QueryDSL Where format)
// - orders: Order[] - sort orders (GOU QueryDSL Order format)
// - select: string[] - fields to return
// - limit: number - max results
// - rerank: { top_n: number } - rerank options
func (ctx *Context) searchDBMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
v8ctx := info.Context()
args := info.Args()
// Validate arguments
if len(args) < 1 {
return bridge.JsException(v8ctx, "DB requires query parameter")
}
// Get query string
if !args[0].IsString() {
return bridge.JsException(v8ctx, "query must be a string")
}
query := args[0].String()
// Parse options (optional)
var opts map[string]interface{}
if len(args) >= 2 && !args[1].IsUndefined() && !args[1].IsNull() {
goVal, err := bridge.GoValue(args[1], v8ctx)
if err != nil {
return bridge.JsException(v8ctx, "invalid options: "+err.Error())
}
if optsMap, ok := goVal.(map[string]interface{}); ok {
opts = optsMap
}
}
// Get search API
searchAPI := ctx.Search()
if searchAPI == nil {
return bridge.JsException(v8ctx, "search API not available")
}
// Execute search
result := searchAPI.DB(query, opts)
// Convert result to JS value
jsVal, err := bridge.JsValue(v8ctx, result)
if err != nil {
return bridge.JsException(v8ctx, "failed to convert result: "+err.Error())
}
return jsVal
})
}
// searchAllMethod implements ctx.search.All(requests)
// Waits for all searches to complete (like Promise.all)
// Each request should have:
// - type: string - "web", "kb", or "db"
// - query: string - search query
// - ... other type-specific options
func (ctx *Context) searchAllMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
v8ctx := info.Context()
args := info.Args()
// Validate arguments
if len(args) < 1 {
return bridge.JsException(v8ctx, "All requires requests parameter")
}
// Parse requests array
goVal, err := bridge.GoValue(args[0], v8ctx)
if err != nil {
return bridge.JsException(v8ctx, "invalid requests: "+err.Error())
}
requestsArray, ok := goVal.([]interface{})
if !ok {
return bridge.JsException(v8ctx, "requests must be an array")
}
// Get search API
searchAPI := ctx.Search()
if searchAPI == nil {
return bridge.JsException(v8ctx, "search API not available")
}
// Execute parallel search
results := searchAPI.All(requestsArray)
// Convert results to JS value
jsVal, err := bridge.JsValue(v8ctx, results)
if err != nil {
return bridge.JsException(v8ctx, "failed to convert results: "+err.Error())
}
return jsVal
})
}
// searchAnyMethod implements ctx.search.Any(requests)
// Returns when 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 (ctx *Context) searchAnyMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
v8ctx := info.Context()
args := info.Args()
// Validate arguments
if len(args) < 1 {
return bridge.JsException(v8ctx, "Any requires requests parameter")
}
// Parse requests array
goVal, err := bridge.GoValue(args[0], v8ctx)
if err != nil {
return bridge.JsException(v8ctx, "invalid requests: "+err.Error())
}
requestsArray, ok := goVal.([]interface{})
if !ok {
return bridge.JsException(v8ctx, "requests must be an array")
}
// Get search API
searchAPI := ctx.Search()
if searchAPI == nil {
return bridge.JsException(v8ctx, "search API not available")
}
// Execute parallel search
results := searchAPI.Any(requestsArray)
// Convert results to JS value
jsVal, err := bridge.JsValue(v8ctx, results)
if err != nil {
return bridge.JsException(v8ctx, "failed to convert results: "+err.Error())
}
return jsVal
})
}
// searchRaceMethod implements ctx.search.Race(requests)
// Returns when 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 (ctx *Context) searchRaceMethod(iso *v8go.Isolate) *v8go.FunctionTemplate {
return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value {
v8ctx := info.Context()
args := info.Args()
// Validate arguments
if len(args) < 1 {
return bridge.JsException(v8ctx, "Race requires requests parameter")
}
// Parse requests array
goVal, err := bridge.GoValue(args[0], v8ctx)
if err != nil {
return bridge.JsException(v8ctx, "invalid requests: "+err.Error())
}
requestsArray, ok := goVal.([]interface{})
if !ok {
return bridge.JsException(v8ctx, "requests must be an array")
}
// Get search API
searchAPI := ctx.Search()
if searchAPI == nil {
return bridge.JsException(v8ctx, "search API not available")
}
// Execute parallel search
results := searchAPI.Race(requestsArray)
// Convert results to JS value
jsVal, err := bridge.JsValue(v8ctx, results)
if err != nil {
return bridge.JsException(v8ctx, "failed to convert results: "+err.Error())
}
return jsVal
})
}