- 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.
362 lines
11 KiB
Go
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
|
|
})
|
|
}
|