yao/agent/caller/process.go
Max 342c2f11a3 Refactor agent call handling and enhance request structures
- Update `forceSkipForSubAgent` to respect caller's `skip.output` setting, allowing for silent internal worker agent execution.
- Refactor error handling in `callAgentWithContext` to utilize a new `NewResult` function for consistent result construction.
- Introduce `ProcessCallRequest` structure for improved agent call parameters, including a default timeout constant for process calls.
2026-02-14 11:30:20 +08:00

159 lines
4.5 KiB
Go

package caller
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/yaoapp/gou/process"
"github.com/yaoapp/kun/exception"
agentContext "github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/openapi/oauth/authorized"
)
func init() {
process.Register("agent.Call", processAgentCall)
}
// processAgentCall implements the agent.Call Process handler.
// Enables agent-to-agent calls from contexts without agent.Context (e.g., YaoJob).
//
// Usage: Process("agent.Call", { assistant_id, messages, model?, ... })
// Returns: *Result (same structure as ctx.agent.Call in JSAPI)
func processAgentCall(p *process.Process) interface{} {
// 1. Parse parameters via struct — fail fast on invalid input
if len(p.Args) == 0 {
exception.New("agent.Call: argument is required", 400).Throw()
}
var req ProcessCallRequest
raw, err := json.Marshal(p.Args[0])
if err != nil {
exception.New("agent.Call: invalid argument: %s", 400, err.Error()).Throw()
}
if err := json.Unmarshal(raw, &req); err != nil {
exception.New("agent.Call: failed to parse request: %s", 400, err.Error()).Throw()
}
if req.AssistantID == "" {
exception.New("agent.Call: assistant_id is required", 400).Throw()
}
if len(req.Messages) == 0 {
exception.New("agent.Call: messages is required", 400).Throw()
}
// 2. Auto-inject authorization info from process context
authInfo := authorized.ProcessAuthInfo(p)
// 3. Build timeout context — LLM calls can take minutes (tool use, multi-turn)
// Default: 10 minutes (DefaultProcessTimeout). Caller can override via `timeout` field.
timeoutSec := req.Timeout
if timeoutSec <= 0 {
timeoutSec = DefaultProcessTimeout
}
parent := p.Context
if parent == nil {
parent = context.Background()
}
timeoutCtx, cancel := context.WithTimeout(parent, time.Duration(timeoutSec)*time.Second)
defer cancel()
// 4. Build headless context + options (encapsulated in context.go)
ctx, opts := NewHeadlessContext(timeoutCtx, authInfo, &req)
defer ctx.Release()
// 5. Parse messages from []map[string]interface{} to []agentContext.Message
messages := ParseMessages(req.Messages)
// 6. Get agent and execute
if AgentGetterFunc == nil {
return NewResult(req.AssistantID, nil, fmt.Errorf("agent getter not initialized"))
}
agent, err := AgentGetterFunc(req.AssistantID)
if err != nil {
return NewResult(req.AssistantID, nil, fmt.Errorf("failed to get agent: %w", err))
}
resp, err := agent.Stream(ctx, messages, opts)
if err != nil {
return NewResult(req.AssistantID, nil, fmt.Errorf("agent call failed: %w", err))
}
// 7. Return *Result — shared with ctx.agent.Call() via NewResult()
return NewResult(req.AssistantID, resp, nil)
}
// ParseMessages converts []map[string]interface{} to []agentContext.Message.
// Extracted as a package-level function so it can be reused by both
// processAgentCall and JSAPI.parseMessages.
func ParseMessages(raw []map[string]interface{}) []agentContext.Message {
result := make([]agentContext.Message, 0, len(raw))
for _, msg := range raw {
ctxMsg := agentContext.Message{}
// Parse role
if role, ok := msg["role"].(string); ok {
ctxMsg.Role = agentContext.MessageRole(role)
}
// Parse content (can be string or array of content parts)
ctxMsg.Content = msg["content"]
// Parse name
if name, ok := msg["name"].(string); ok {
ctxMsg.Name = &name
}
// Parse tool_call_id
if toolCallID, ok := msg["tool_call_id"].(string); ok {
ctxMsg.ToolCallID = &toolCallID
}
// Parse tool_calls
if toolCalls, ok := msg["tool_calls"].([]interface{}); ok {
ctxMsg.ToolCalls = parseToolCalls(toolCalls)
}
// Parse refusal
if refusal, ok := msg["refusal"].(string); ok {
ctxMsg.Refusal = &refusal
}
result = append(result, ctxMsg)
}
return result
}
// parseToolCalls converts []interface{} to []agentContext.ToolCall
func parseToolCalls(toolCalls []interface{}) []agentContext.ToolCall {
result := make([]agentContext.ToolCall, 0, len(toolCalls))
for _, tc := range toolCalls {
tcMap, ok := tc.(map[string]interface{})
if !ok {
continue
}
toolCall := agentContext.ToolCall{}
if id, ok := tcMap["id"].(string); ok {
toolCall.ID = id
}
if tcType, ok := tcMap["type"].(string); ok {
toolCall.Type = agentContext.ToolCallType(tcType)
}
if fn, ok := tcMap["function"].(map[string]interface{}); ok {
if name, ok := fn["name"].(string); ok {
toolCall.Function.Name = name
}
if args, ok := fn["arguments"].(string); ok {
toolCall.Function.Arguments = args
}
}
result = append(result, toolCall)
}
return result
}