- Updated the Stream, BuildContent, and LLM execution methods to accept an Options parameter, allowing for more flexible context handling. - Removed debug print statements to clean up the code and improve readability. - Enhanced locale handling in the loadMap function to automatically inject assistant name and description into all locales, ensuring better localization support. - Introduced output skipping functionality in context options to manage internal A2A calls more effectively. - Improved output writer resolution logic to prioritize context settings, enhancing output management during agent calls.
188 lines
6 KiB
Go
188 lines
6 KiB
Go
package content
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
|
|
jsoniter "github.com/json-iterator/go"
|
|
"github.com/yaoapp/gou/mcp"
|
|
"github.com/yaoapp/kun/log"
|
|
agentContext "github.com/yaoapp/yao/agent/context"
|
|
)
|
|
|
|
// AgentCaller interface for calling agents (to avoid circular dependency)
|
|
type AgentCaller interface {
|
|
Stream(ctx *agentContext.Context, messages []agentContext.Message, options ...*agentContext.Options) (interface{}, error)
|
|
}
|
|
|
|
// AgentGetterFunc is a function type that gets an agent by ID
|
|
var AgentGetterFunc func(agentID string) (AgentCaller, error)
|
|
|
|
// CallAgent calls an agent to process content (vision, audio, etc.)
|
|
// This is a generic function that can be used by any handler
|
|
func CallAgent(ctx *agentContext.Context, agentID string, message agentContext.Message) (string, error) {
|
|
if AgentGetterFunc == nil {
|
|
return "", fmt.Errorf("AgentGetterFunc not initialized")
|
|
}
|
|
|
|
// Load the agent by ID using the injected function
|
|
agent, err := AgentGetterFunc(agentID)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to load agent %s: %w", agentID, err)
|
|
}
|
|
|
|
// Call the agent with the message
|
|
messages := []agentContext.Message{message}
|
|
|
|
// Note: Connector is now in Options (call-level parameter), not Context
|
|
// For A2A calls, skip history and output (we only need the response data)
|
|
opts := &agentContext.Options{Skip: &agentContext.Skip{History: true, Output: true}} // Skip history and output
|
|
response, err := agent.Stream(ctx, messages, opts)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to call agent %s: %w", agentID, err)
|
|
}
|
|
|
|
// Extract text from agent response
|
|
// Two formats are supported:
|
|
// 1. Custom Hook response (from Next hook)
|
|
// 2. Standard Agent Stream response (LLM completion)
|
|
|
|
return extractTextFromAgentResponse(response)
|
|
}
|
|
|
|
// extractTextFromAgentResponse extracts text from agent response
|
|
// Handles two response formats:
|
|
// 1. Custom Hook response: if it's a string, return directly; otherwise JSON stringify
|
|
// 2. Standard response: extract from completion.content
|
|
func extractTextFromAgentResponse(response interface{}) (string, error) {
|
|
if response == nil {
|
|
return "", fmt.Errorf("agent returned nil response")
|
|
}
|
|
|
|
// Try to parse as standard response format (has "completion" field with LLM result)
|
|
if responseMap, ok := response.(map[string]interface{}); ok {
|
|
// Check for completion field (standard LLM response)
|
|
if completion, hasCompletion := responseMap["completion"]; hasCompletion {
|
|
if completionMap, ok := completion.(map[string]interface{}); ok {
|
|
// Extract content from completion
|
|
if content, hasContent := completionMap["content"]; hasContent {
|
|
// Content can be string or structured
|
|
switch v := content.(type) {
|
|
case string:
|
|
return v, nil
|
|
case []interface{}:
|
|
// Handle multimodal content array
|
|
var text string
|
|
for _, part := range v {
|
|
if partMap, ok := part.(map[string]interface{}); ok {
|
|
if partType, _ := partMap["type"].(string); partType == "text" {
|
|
if textContent, ok := partMap["text"].(string); ok {
|
|
text += textContent
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if text != "" {
|
|
return text, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for data field (custom hook response with data wrapper)
|
|
if data, hasData := responseMap["data"]; hasData {
|
|
// If data is a string, return directly
|
|
if dataStr, ok := data.(string); ok {
|
|
return dataStr, nil
|
|
}
|
|
// Otherwise, JSON stringify
|
|
jsonBytes, err := jsoniter.Marshal(data)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to serialize hook data response: %w", err)
|
|
}
|
|
return string(jsonBytes), nil
|
|
}
|
|
|
|
// If the map itself looks like content, try to extract
|
|
// This handles cases where the response is the content directly
|
|
if content, hasContent := responseMap["content"]; hasContent {
|
|
if contentStr, ok := content.(string); ok {
|
|
return contentStr, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Custom Hook response: if it's a plain string, return directly
|
|
if responseStr, ok := response.(string); ok {
|
|
return responseStr, nil
|
|
}
|
|
|
|
// Otherwise, JSON stringify the response
|
|
jsonBytes, err := jsoniter.Marshal(response)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to serialize agent response: %w", err)
|
|
}
|
|
return string(jsonBytes), nil
|
|
}
|
|
|
|
// CallMCPTool calls an MCP tool to process content
|
|
// This is a generic function that can be used by any handler
|
|
func CallMCPTool(ctx *agentContext.Context, serverID string, toolName string, arguments map[string]interface{}) (string, error) {
|
|
// Get MCP context for cancellation/timeout control
|
|
mcpCtx := ctx.Context
|
|
if mcpCtx == nil {
|
|
mcpCtx = context.Background()
|
|
}
|
|
|
|
// Get MCP client
|
|
client, err := mcp.Select(serverID)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to select MCP client '%s': %w", serverID, err)
|
|
}
|
|
|
|
// Call the tool
|
|
log.Trace("[Content] Calling MCP tool: %s (server: %s)", toolName, serverID)
|
|
callResult, err := client.CallTool(mcpCtx, toolName, arguments)
|
|
if err != nil {
|
|
return "", fmt.Errorf("MCP tool call failed: %w", err)
|
|
}
|
|
|
|
// Check if result is an error
|
|
if callResult.IsError {
|
|
return "", fmt.Errorf("MCP tool returned error: %v", callResult.Content)
|
|
}
|
|
|
|
// Extract text content from result
|
|
// callResult.Content is []ToolContent
|
|
var text string
|
|
for _, content := range callResult.Content {
|
|
if content.Type == "text" {
|
|
text += content.Text
|
|
}
|
|
// Can also handle other types like image, resource if needed
|
|
}
|
|
|
|
if text == "" {
|
|
// If no text content found, return error
|
|
return "", fmt.Errorf("MCP tool returned no text content")
|
|
}
|
|
|
|
return text, nil
|
|
}
|
|
|
|
// EncodeToBase64DataURI encodes data to base64 with data URI prefix
|
|
// This is useful for encoding images, audio, or other binary data
|
|
func EncodeToBase64DataURI(data []byte, contentType string) string {
|
|
// Ensure we have a valid content type
|
|
if contentType == "" {
|
|
contentType = "application/octet-stream" // default
|
|
}
|
|
|
|
// Encode to base64
|
|
encoded := base64.StdEncoding.EncodeToString(data)
|
|
|
|
// Return data URI format
|
|
return fmt.Sprintf("data:%s;base64,%s", contentType, encoded)
|
|
}
|