- Updated the agent's Stream and response processing methods to return and handle *context.Response directly, eliminating the need for type assertions. - Simplified test cases by removing unnecessary type conversions and directly accessing response fields. - Enhanced the extraction of data from Next hook responses, ensuring more robust handling of custom data structures. - Improved overall code readability and maintainability by streamlining response handling logic across various components.
237 lines
6 KiB
Go
237 lines
6 KiB
Go
package web
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/yaoapp/yao/agent/caller"
|
|
agentContext "github.com/yaoapp/yao/agent/context"
|
|
"github.com/yaoapp/yao/agent/search/types"
|
|
)
|
|
|
|
// AgentProvider implements web search using another agent (AI Search)
|
|
type AgentProvider struct {
|
|
agentID string // Agent/Assistant ID (e.g., "workers.search.web")
|
|
}
|
|
|
|
// NewAgentProvider creates a new Agent provider
|
|
func NewAgentProvider(agentID string) *AgentProvider {
|
|
return &AgentProvider{
|
|
agentID: agentID,
|
|
}
|
|
}
|
|
|
|
// Search executes web search via agent delegation
|
|
// The agent can understand intent, generate optimized queries, and return structured results
|
|
func (p *AgentProvider) Search(ctx *agentContext.Context, req *types.Request) (*types.Result, error) {
|
|
startTime := time.Now()
|
|
|
|
// Check if context is provided
|
|
if ctx == nil {
|
|
return &types.Result{
|
|
Type: types.SearchTypeWeb,
|
|
Query: req.Query,
|
|
Source: req.Source,
|
|
Items: []*types.ResultItem{},
|
|
Total: 0,
|
|
Duration: time.Since(startTime).Milliseconds(),
|
|
Error: "Agent mode requires context",
|
|
}, nil
|
|
}
|
|
|
|
// Check if AgentGetterFunc is initialized
|
|
if caller.AgentGetterFunc == nil {
|
|
return &types.Result{
|
|
Type: types.SearchTypeWeb,
|
|
Query: req.Query,
|
|
Source: req.Source,
|
|
Items: []*types.ResultItem{},
|
|
Total: 0,
|
|
Duration: time.Since(startTime).Milliseconds(),
|
|
Error: "AgentGetterFunc not initialized",
|
|
}, nil
|
|
}
|
|
|
|
// Get the agent
|
|
agent, err := caller.AgentGetterFunc(p.agentID)
|
|
if err != nil {
|
|
return &types.Result{
|
|
Type: types.SearchTypeWeb,
|
|
Query: req.Query,
|
|
Source: req.Source,
|
|
Items: []*types.ResultItem{},
|
|
Total: 0,
|
|
Duration: time.Since(startTime).Milliseconds(),
|
|
Error: fmt.Sprintf("Agent '%s' not found: %v", p.agentID, err),
|
|
}, nil
|
|
}
|
|
|
|
// Build message for the agent
|
|
// Include search parameters in the message content
|
|
searchParams := map[string]interface{}{
|
|
"query": req.Query,
|
|
"type": "web",
|
|
"source": string(req.Source),
|
|
}
|
|
|
|
if req.Limit > 0 {
|
|
searchParams["limit"] = req.Limit
|
|
}
|
|
if len(req.Sites) > 0 {
|
|
searchParams["sites"] = req.Sites
|
|
}
|
|
if req.TimeRange != "" {
|
|
searchParams["time_range"] = req.TimeRange
|
|
}
|
|
|
|
// Convert to JSON for the message
|
|
paramsJSON, err := json.Marshal(searchParams)
|
|
if err != nil {
|
|
return &types.Result{
|
|
Type: types.SearchTypeWeb,
|
|
Query: req.Query,
|
|
Source: req.Source,
|
|
Items: []*types.ResultItem{},
|
|
Total: 0,
|
|
Duration: time.Since(startTime).Milliseconds(),
|
|
Error: fmt.Sprintf("Failed to serialize search params: %v", err),
|
|
}, nil
|
|
}
|
|
|
|
// Create message for the agent
|
|
message := agentContext.Message{
|
|
Role: "user",
|
|
Content: string(paramsJSON),
|
|
}
|
|
|
|
// Call the agent with skip options (no history, no output)
|
|
opts := &agentContext.Options{
|
|
Skip: &agentContext.Skip{
|
|
History: true,
|
|
Output: true,
|
|
},
|
|
}
|
|
|
|
response, err := agent.Stream(ctx, []agentContext.Message{message}, opts)
|
|
if err != nil {
|
|
return &types.Result{
|
|
Type: types.SearchTypeWeb,
|
|
Query: req.Query,
|
|
Source: req.Source,
|
|
Items: []*types.ResultItem{},
|
|
Total: 0,
|
|
Duration: time.Since(startTime).Milliseconds(),
|
|
Error: fmt.Sprintf("Agent call failed: %v", err),
|
|
}, nil
|
|
}
|
|
|
|
// Parse the agent response
|
|
items, total, parseErr := p.parseAgentResponse(response, req.Source)
|
|
if parseErr != "" {
|
|
return &types.Result{
|
|
Type: types.SearchTypeWeb,
|
|
Query: req.Query,
|
|
Source: req.Source,
|
|
Items: []*types.ResultItem{},
|
|
Total: 0,
|
|
Duration: time.Since(startTime).Milliseconds(),
|
|
Error: parseErr,
|
|
}, nil
|
|
}
|
|
|
|
return &types.Result{
|
|
Type: types.SearchTypeWeb,
|
|
Query: req.Query,
|
|
Source: req.Source,
|
|
Items: items,
|
|
Total: total,
|
|
Duration: time.Since(startTime).Milliseconds(),
|
|
}, nil
|
|
}
|
|
|
|
// parseAgentResponse parses the agent's *context.Response into search result items
|
|
// Now that agent.Stream() returns *context.Response directly,
|
|
// we can access fields without type assertions.
|
|
//
|
|
// The agent returns search results in response.Next field
|
|
func (p *AgentProvider) parseAgentResponse(response *agentContext.Response, source types.SourceType) ([]*types.ResultItem, int, string) {
|
|
if response == nil || response.Next == nil {
|
|
return nil, 0, "Agent returned nil response"
|
|
}
|
|
|
|
// Extract data from Next field
|
|
data := extractNextData(response.Next)
|
|
if data == nil {
|
|
return nil, 0, "Failed to extract data from agent response"
|
|
}
|
|
|
|
// Extract items from data
|
|
items := []*types.ResultItem{}
|
|
total := 0
|
|
|
|
if itemsData, ok := data["items"].([]interface{}); ok {
|
|
for _, itemData := range itemsData {
|
|
if item, ok := itemData.(map[string]interface{}); ok {
|
|
resultItem := &types.ResultItem{
|
|
Type: types.SearchTypeWeb,
|
|
Source: source,
|
|
}
|
|
|
|
if title, ok := item["title"].(string); ok {
|
|
resultItem.Title = title
|
|
}
|
|
if content, ok := item["content"].(string); ok {
|
|
resultItem.Content = content
|
|
}
|
|
if url, ok := item["url"].(string); ok {
|
|
resultItem.URL = url
|
|
}
|
|
if score, ok := item["score"].(float64); ok {
|
|
resultItem.Score = score
|
|
}
|
|
|
|
items = append(items, resultItem)
|
|
}
|
|
}
|
|
}
|
|
|
|
if totalVal, ok := data["total"].(float64); ok {
|
|
total = int(totalVal)
|
|
} else {
|
|
total = len(items)
|
|
}
|
|
|
|
return items, total, ""
|
|
}
|
|
|
|
// extractNextData extracts the actual data from response.Next field
|
|
// Handles nested structures like { "data": { ... } }
|
|
func extractNextData(next interface{}) map[string]interface{} {
|
|
if next == nil {
|
|
return nil
|
|
}
|
|
|
|
switch v := next.(type) {
|
|
case map[string]interface{}:
|
|
// Check for "data" wrapper
|
|
if data, ok := v["data"].(map[string]interface{}); ok {
|
|
return data
|
|
}
|
|
return v
|
|
case string:
|
|
// Try to parse as JSON
|
|
var data map[string]interface{}
|
|
if err := json.Unmarshal([]byte(v), &data); err == nil {
|
|
return extractNextData(data)
|
|
}
|
|
}
|
|
// Try to handle other types by converting to JSON and back
|
|
if bytes, err := json.Marshal(next); err == nil {
|
|
var data map[string]interface{}
|
|
if err := json.Unmarshal(bytes, &data); err == nil {
|
|
return extractNextData(data)
|
|
}
|
|
}
|
|
return nil
|
|
}
|