feat(agent): implement tool loop processing and enhance error handling
- Added support for tool loop processing when tool call responses are present and sandbox mode is disabled, improving the assistant's ability to handle complex tool interactions. - Implemented fallback delegation to a loop fallback mechanism in case of tool loop execution failure, enhancing robustness in error scenarios. - Updated the assistant message structure to include reasoning content, providing better context for generated responses. - Enhanced error logging in tool call execution to include detailed content, improving diagnostics for tool call failures. - Updated system configuration to include a new loop fallback agent, expanding the assistant's capabilities.
This commit is contained in:
parent
7da06a4ae1
commit
fb01a1c141
10 changed files with 867 additions and 420 deletions
|
|
@ -571,11 +571,47 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa
|
|||
ast.sendStreamEndOnError(ctx, streamHandler, streamStartTime, err)
|
||||
return nil, err
|
||||
}
|
||||
} else if len(toolCallResponses) > 0 && !ast.HasSandbox() && !ast.isToolLoopDisabled() {
|
||||
// No Next hook + has tool results + not sandbox → tool loop
|
||||
ctx.Logger.Debug("Entering tool loop for tool result processing")
|
||||
loopResponse, loopCompletion, loopTools, err := ast.executeToolLoop(ctx, &ToolLoopParams{
|
||||
CompletionMessages: completionMessages,
|
||||
CompletionOptions: completionOptions,
|
||||
CompletionResponse: completionResponse,
|
||||
ToolCallResponses: toolCallResponses,
|
||||
FullMessages: fullMessages,
|
||||
AgentNode: agentNode,
|
||||
StreamHandler: streamHandler,
|
||||
CreateResponse: createResponse,
|
||||
Opts: opts,
|
||||
})
|
||||
if err != nil {
|
||||
// Fallback to __yao.loop_fallback delegation
|
||||
ctx.Logger.Warn("Tool loop failed: %v, falling back to loop_fallback", err)
|
||||
fallbackDelegate := ast.buildLoopFallbackDelegate(ctx, fullMessages, completionResponse, toolCallResponses)
|
||||
delegateResponse, delegateErr := ast.handleDelegation(ctx, fallbackDelegate, streamHandler)
|
||||
if delegateErr != nil {
|
||||
ctx.Logger.Warn("loop_fallback also failed: %v, using standard response", delegateErr)
|
||||
finalResponse = ast.buildStandardResponse(&NextProcessContext{
|
||||
Context: ctx,
|
||||
CompletionResponse: completionResponse,
|
||||
FullMessages: fullMessages,
|
||||
ToolCallResponses: toolCallResponses,
|
||||
StreamHandler: streamHandler,
|
||||
CreateResponse: createResponse,
|
||||
})
|
||||
} else {
|
||||
finalResponse = delegateResponse
|
||||
}
|
||||
} else {
|
||||
completionResponse = loopCompletion
|
||||
toolCallResponses = loopTools
|
||||
finalResponse = loopResponse
|
||||
}
|
||||
} else {
|
||||
// No Next hook: use standard response
|
||||
// No tool calls, sandbox mode, or loop disabled: standard response
|
||||
finalResponse = ast.buildStandardResponse(&NextProcessContext{
|
||||
Context: ctx,
|
||||
NextResponse: nil,
|
||||
CompletionResponse: completionResponse,
|
||||
FullMessages: fullMessages,
|
||||
ToolCallResponses: toolCallResponses,
|
||||
|
|
@ -801,9 +837,10 @@ func (ast *Assistant) buildToolRetryMessages(
|
|||
|
||||
// Add assistant message with tool calls
|
||||
assistantMsg := context.Message{
|
||||
Role: context.RoleAssistant,
|
||||
Content: completionResponse.Content,
|
||||
ToolCalls: completionResponse.ToolCalls,
|
||||
Role: context.RoleAssistant,
|
||||
Content: completionResponse.Content,
|
||||
ReasoningContent: completionResponse.ReasoningContent,
|
||||
ToolCalls: completionResponse.ToolCalls,
|
||||
}
|
||||
retryMessages = append(retryMessages, assistantMsg)
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ var systemAgents = []string{
|
|||
"entity",
|
||||
"vision",
|
||||
"fetch",
|
||||
"loop_fallback",
|
||||
}
|
||||
|
||||
// SystemConfig holds the system agents connector configuration
|
||||
|
|
@ -42,13 +43,14 @@ type SystemConfig struct {
|
|||
Heavy string // Default connector for the "heavy" role (complex reasoning)
|
||||
|
||||
// Per-agent overrides (consumed by resolveSystemConnector → ast.Connector)
|
||||
Keyword string // Connector for __yao.keyword agent
|
||||
QueryDSL string // Connector for __yao.querydsl agent
|
||||
Title string // Connector for __yao.title agent
|
||||
Prompt string // Connector for __yao.prompt agent
|
||||
RobotPrompt string // Connector for __yao.robot_prompt agent
|
||||
NeedSearch string // Connector for __yao.needsearch agent
|
||||
Entity string // Connector for __yao.entity agent
|
||||
Keyword string // Connector for __yao.keyword agent
|
||||
QueryDSL string // Connector for __yao.querydsl agent
|
||||
Title string // Connector for __yao.title agent
|
||||
Prompt string // Connector for __yao.prompt agent
|
||||
RobotPrompt string // Connector for __yao.robot_prompt agent
|
||||
NeedSearch string // Connector for __yao.needsearch agent
|
||||
Entity string // Connector for __yao.entity agent
|
||||
LoopFallback string // Connector for __yao.loop_fallback agent
|
||||
}
|
||||
|
||||
// systemConfig holds the system agents configuration (global variable like others in load.go)
|
||||
|
|
@ -237,6 +239,8 @@ func resolveSystemConnector(agentID string) string {
|
|||
return systemConfig.Vision
|
||||
case "__yao.audio":
|
||||
return systemConfig.Audio
|
||||
case "__yao.loop_fallback":
|
||||
return systemConfig.LoopFallback
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
|
|||
295
agent/assistant/loop.go
Normal file
295
agent/assistant/loop.go
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
package assistant
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/yaoapp/yao/agent/context"
|
||||
"github.com/yaoapp/yao/agent/output/message"
|
||||
"github.com/yaoapp/yao/trace/types"
|
||||
)
|
||||
|
||||
// ToolLoopParams holds all parameters needed by executeToolLoop.
|
||||
type ToolLoopParams struct {
|
||||
CompletionMessages []context.Message
|
||||
CompletionOptions *context.CompletionOptions
|
||||
CompletionResponse *context.CompletionResponse
|
||||
ToolCallResponses []context.ToolCallResponse
|
||||
FullMessages []context.Message
|
||||
AgentNode types.Node
|
||||
StreamHandler message.StreamFunc
|
||||
CreateResponse *context.HookCreateResponse
|
||||
Opts *context.Options
|
||||
}
|
||||
|
||||
// executeToolLoop feeds tool results back to the LLM in a loop until
|
||||
// the LLM produces a final text response (no more tool_calls) or
|
||||
// the maximum number of turns is reached.
|
||||
//
|
||||
// Returns the final Response, the last CompletionResponse (for tracing),
|
||||
// accumulated ToolCallResponses, and any error.
|
||||
func (ast *Assistant) executeToolLoop(
|
||||
ctx *context.Context,
|
||||
params *ToolLoopParams,
|
||||
) (*context.Response, *context.CompletionResponse, []context.ToolCallResponse, error) {
|
||||
|
||||
maxTurns := ast.getMaxToolLoopTurns()
|
||||
currentMessages := params.CompletionMessages
|
||||
currentCompletion := params.CompletionResponse
|
||||
allToolResponses := make([]context.ToolCallResponse, 0, len(params.ToolCallResponses))
|
||||
allToolResponses = append(allToolResponses, params.ToolCallResponses...)
|
||||
|
||||
for turn := 0; turn < maxTurns; turn++ {
|
||||
ctx.Logger.Debug("Tool loop turn %d/%d", turn+1, maxTurns)
|
||||
|
||||
// Build messages: previous messages + assistant(tool_calls) + tool results
|
||||
loopMessages := buildToolLoopMessages(currentMessages, currentCompletion, allToolResponses[len(allToolResponses)-len(params.ToolCallResponses):])
|
||||
|
||||
// Step tracking: LLM call
|
||||
ast.BeginStep(ctx, context.StepTypeLLM, map[string]interface{}{
|
||||
"messages": loopMessages,
|
||||
"loop_turn": turn + 1,
|
||||
})
|
||||
|
||||
// Call LLM with tool results included
|
||||
newCompletion, err := ast.executeLLMStream(ctx, loopMessages, params.CompletionOptions, params.AgentNode, params.StreamHandler, params.Opts)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("tool loop LLM call failed (turn %d): %w", turn+1, err)
|
||||
}
|
||||
|
||||
ast.CompleteStep(ctx, map[string]interface{}{
|
||||
"content": newCompletion.Content,
|
||||
"tool_calls": newCompletion.ToolCalls,
|
||||
})
|
||||
|
||||
// No tool_calls → LLM gave final text response
|
||||
if newCompletion.ToolCalls == nil || len(newCompletion.ToolCalls) == 0 {
|
||||
finalResponse := ast.buildStandardResponse(&NextProcessContext{
|
||||
Context: ctx,
|
||||
CompletionResponse: newCompletion,
|
||||
FullMessages: params.FullMessages,
|
||||
ToolCallResponses: allToolResponses,
|
||||
StreamHandler: params.StreamHandler,
|
||||
CreateResponse: params.CreateResponse,
|
||||
})
|
||||
return finalResponse, newCompletion, allToolResponses, nil
|
||||
}
|
||||
|
||||
// Has tool_calls → execute them
|
||||
ast.BeginStep(ctx, context.StepTypeTool, map[string]interface{}{
|
||||
"tool_calls": newCompletion.ToolCalls,
|
||||
"loop_turn": turn + 1,
|
||||
})
|
||||
|
||||
toolResults, _ := ast.executeToolCalls(ctx, newCompletion.ToolCalls, 0)
|
||||
|
||||
// Convert ToolCallResult → ToolCallResponse
|
||||
toolCallArgsMap := make(map[string]interface{})
|
||||
for _, tc := range newCompletion.ToolCalls {
|
||||
toolCallArgsMap[tc.ID] = tc.Function.Arguments
|
||||
}
|
||||
|
||||
turnResponses := make([]context.ToolCallResponse, len(toolResults))
|
||||
for i, result := range toolResults {
|
||||
parsedContent, _ := result.ParsedContent()
|
||||
turnResponses[i] = context.ToolCallResponse{
|
||||
ToolCallID: result.ToolCallID,
|
||||
Server: result.Server(),
|
||||
Tool: result.Tool(),
|
||||
Arguments: toolCallArgsMap[result.ToolCallID],
|
||||
Result: parsedContent,
|
||||
Error: "",
|
||||
}
|
||||
if result.Error != nil {
|
||||
turnResponses[i].Error = result.Error.Error()
|
||||
}
|
||||
}
|
||||
|
||||
ast.CompleteStep(ctx, map[string]interface{}{
|
||||
"results": turnResponses,
|
||||
"loop_turn": turn + 1,
|
||||
})
|
||||
|
||||
// Accumulate and prepare next iteration
|
||||
allToolResponses = append(allToolResponses, turnResponses...)
|
||||
currentMessages = loopMessages
|
||||
currentCompletion = newCompletion
|
||||
params.ToolCallResponses = turnResponses
|
||||
}
|
||||
|
||||
return nil, nil, allToolResponses, fmt.Errorf("tool loop reached max turns (%d)", maxTurns)
|
||||
}
|
||||
|
||||
// buildToolLoopMessages constructs the message sequence for the next LLM call:
|
||||
// previous messages + assistant message (with tool_calls) + tool result messages.
|
||||
// Unlike buildToolRetryMessages, this does NOT append a retry system prompt.
|
||||
func buildToolLoopMessages(
|
||||
previousMessages []context.Message,
|
||||
completion *context.CompletionResponse,
|
||||
toolResponses []context.ToolCallResponse,
|
||||
) []context.Message {
|
||||
messages := make([]context.Message, 0, len(previousMessages)+len(toolResponses)+2)
|
||||
messages = append(messages, previousMessages...)
|
||||
|
||||
// Assistant message with tool_calls
|
||||
messages = append(messages, context.Message{
|
||||
Role: context.RoleAssistant,
|
||||
Content: completion.Content,
|
||||
ReasoningContent: completion.ReasoningContent,
|
||||
ToolCalls: completion.ToolCalls,
|
||||
})
|
||||
|
||||
// One tool-role message per tool call result
|
||||
for _, tr := range toolResponses {
|
||||
var content string
|
||||
if tr.Error != "" {
|
||||
content = fmt.Sprintf("Error: %s", tr.Error)
|
||||
} else if tr.Result != nil {
|
||||
raw, _ := jsoniter.MarshalToString(tr.Result)
|
||||
content = raw
|
||||
}
|
||||
toolCallID := tr.ToolCallID
|
||||
messages = append(messages, context.Message{
|
||||
Role: context.RoleTool,
|
||||
Content: content,
|
||||
ToolCallID: &toolCallID,
|
||||
})
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
// isToolLoopDisabled checks mcp.options.tool_loop.
|
||||
// Default is enabled (returns false). Only disabled when explicitly set to false.
|
||||
func (ast *Assistant) isToolLoopDisabled() bool {
|
||||
if ast.MCP == nil || ast.MCP.Options == nil {
|
||||
return false
|
||||
}
|
||||
if v, ok := ast.MCP.Options["tool_loop"]; ok {
|
||||
if enabled, ok := v.(bool); ok {
|
||||
return !enabled
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getMaxToolLoopTurns reads mcp.options.max_turn. Default is 5.
|
||||
func (ast *Assistant) getMaxToolLoopTurns() int {
|
||||
const defaultMaxTurns = 5
|
||||
if ast.MCP == nil || ast.MCP.Options == nil {
|
||||
return defaultMaxTurns
|
||||
}
|
||||
if v, ok := ast.MCP.Options["max_turn"]; ok {
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
if n > 0 {
|
||||
return int(n)
|
||||
}
|
||||
case int:
|
||||
if n > 0 {
|
||||
return n
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultMaxTurns
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fallback: __yao.loop_fallback delegation (used when tool loop fails/maxes out)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// buildLoopFallbackDelegate constructs a DelegateConfig for __yao.loop_fallback.
|
||||
// It packages conversation context and tool results into a Markdown user message.
|
||||
func (ast *Assistant) buildLoopFallbackDelegate(
|
||||
ctx *context.Context,
|
||||
fullMessages []context.Message,
|
||||
completion *context.CompletionResponse,
|
||||
toolResults []context.ToolCallResponse,
|
||||
) *context.DelegateConfig {
|
||||
|
||||
content := buildLoopFallbackMarkdown(fullMessages, toolResults)
|
||||
return &context.DelegateConfig{
|
||||
AgentID: "__yao.loop_fallback",
|
||||
Messages: []context.Message{
|
||||
{Role: context.RoleUser, Content: content},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// buildLoopFallbackMarkdown formats context into a Markdown string for the fallback agent.
|
||||
func buildLoopFallbackMarkdown(
|
||||
fullMessages []context.Message,
|
||||
toolResults []context.ToolCallResponse,
|
||||
) string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString("## Assistant Context\n\n")
|
||||
for _, msg := range fullMessages {
|
||||
if msg.Role == context.RoleSystem {
|
||||
if text := messageText(msg); text != "" {
|
||||
sb.WriteString(text)
|
||||
sb.WriteString("\n\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("## Conversation\n\n")
|
||||
for _, msg := range fullMessages {
|
||||
text := messageText(msg)
|
||||
switch msg.Role {
|
||||
case context.RoleUser:
|
||||
if text != "" {
|
||||
sb.WriteString(fmt.Sprintf("**User**: %s\n\n", text))
|
||||
}
|
||||
case context.RoleAssistant:
|
||||
if text != "" {
|
||||
sb.WriteString(fmt.Sprintf("**Assistant**: %s\n\n", text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("## Tool Results\n\n")
|
||||
for _, tr := range toolResults {
|
||||
toolName := tr.Tool
|
||||
if tr.Server != "" {
|
||||
toolName = tr.Server + "." + tr.Tool
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("### %s\n", toolName))
|
||||
if tr.Error != "" {
|
||||
sb.WriteString(fmt.Sprintf("Error: %s\n\n", tr.Error))
|
||||
} else {
|
||||
raw, _ := jsoniter.MarshalToString(tr.Result)
|
||||
sb.WriteString(fmt.Sprintf("```json\n%s\n```\n\n", raw))
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("---\nPlease answer the user's question based on the above context and tool results.\n")
|
||||
sb.WriteString("Respond in the same language as the user.\n")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// messageText extracts text content from a message's Content field.
|
||||
// Content can be a string or an array of content parts (multimodal).
|
||||
func messageText(msg context.Message) string {
|
||||
if msg.Content == nil {
|
||||
return ""
|
||||
}
|
||||
if str, ok := msg.Content.(string); ok {
|
||||
return str
|
||||
}
|
||||
if parts, ok := msg.Content.([]interface{}); ok {
|
||||
var texts []string
|
||||
for _, part := range parts {
|
||||
if partMap, ok := part.(map[string]interface{}); ok {
|
||||
if partMap["type"] == "text" {
|
||||
if text, ok := partMap["text"].(string); ok {
|
||||
texts = append(texts, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.Join(texts, "\n")
|
||||
}
|
||||
return fmt.Sprintf("%v", msg.Content)
|
||||
}
|
||||
|
|
@ -375,12 +375,6 @@ func (ast *Assistant) executeSingleToolCall(ctx *agentContext.Context, toolCall
|
|||
return []ToolCallResult{result}, true
|
||||
}
|
||||
|
||||
// Check if result is an error
|
||||
if callResult.IsError {
|
||||
result.Error = fmt.Errorf("MCP tool error")
|
||||
result.IsRetryableError = false // MCP internal error is not retryable
|
||||
}
|
||||
|
||||
// Serialize the Content field only ([]ToolContent)
|
||||
contentBytes, err := jsoniter.Marshal(callResult.Content)
|
||||
if err != nil {
|
||||
|
|
@ -396,6 +390,19 @@ func (ast *Assistant) executeSingleToolCall(ctx *agentContext.Context, toolCall
|
|||
}
|
||||
|
||||
result.Content = string(contentBytes)
|
||||
|
||||
// Check if result is an error — include actual content so LLM can see the details
|
||||
if callResult.IsError {
|
||||
result.Error = fmt.Errorf("tool call error: %s", result.Content)
|
||||
result.IsRetryableError = isRetryableToolError(result.Error)
|
||||
ctx.Logger.Error("Tool call failed: %s - %s (retryable: %v)", toolCall.Function.Name, result.Content, result.IsRetryableError)
|
||||
ctx.Logger.ToolComplete(toolCall.Function.Name, false)
|
||||
if toolNode != nil {
|
||||
toolNode.Fail(result.Error)
|
||||
}
|
||||
return []ToolCallResult{result}, true
|
||||
}
|
||||
|
||||
ctx.Logger.ToolComplete(toolCall.Function.Name, true)
|
||||
|
||||
if toolNode != nil {
|
||||
|
|
@ -809,19 +816,12 @@ func (ast *Assistant) executeServerToolsSequentialWithTrace(mcpCtx context.Conte
|
|||
toolNode.Fail(err)
|
||||
}
|
||||
} else {
|
||||
// Check if result is an error
|
||||
if mcpResult.IsError {
|
||||
result.Error = fmt.Errorf("MCP tool error")
|
||||
result.IsRetryableError = false // MCP internal error is not retryable
|
||||
hasErrors = true
|
||||
}
|
||||
|
||||
// Serialize the Content field only ([]ToolContent)
|
||||
contentBytes, err := jsoniter.Marshal(mcpResult.Content)
|
||||
if err != nil {
|
||||
result.Error = err
|
||||
result.Content = fmt.Sprintf("Failed to serialize result: %v", err)
|
||||
result.IsRetryableError = false // Serialization error is not retryable
|
||||
result.IsRetryableError = false
|
||||
hasErrors = true
|
||||
ctx.Logger.ToolComplete(tc.Function.Name, false)
|
||||
if toolNode != nil {
|
||||
|
|
@ -829,11 +829,24 @@ func (ast *Assistant) executeServerToolsSequentialWithTrace(mcpCtx context.Conte
|
|||
}
|
||||
} else {
|
||||
result.Content = string(contentBytes)
|
||||
ctx.Logger.ToolComplete(tc.Function.Name, !mcpResult.IsError)
|
||||
if toolNode != nil {
|
||||
toolNode.Complete(map[string]any{
|
||||
"result": mcpResult.Content,
|
||||
})
|
||||
|
||||
// Check if result is an error — include actual content so LLM can see the details
|
||||
if mcpResult.IsError {
|
||||
result.Error = fmt.Errorf("tool call error: %s", result.Content)
|
||||
result.IsRetryableError = isRetryableToolError(result.Error)
|
||||
hasErrors = true
|
||||
ctx.Logger.Error("Tool call failed: %s - %s (retryable: %v)", toolName, result.Content, result.IsRetryableError)
|
||||
ctx.Logger.ToolComplete(tc.Function.Name, false)
|
||||
if toolNode != nil {
|
||||
toolNode.Fail(result.Error)
|
||||
}
|
||||
} else {
|
||||
ctx.Logger.ToolComplete(tc.Function.Name, true)
|
||||
if toolNode != nil {
|
||||
toolNode.Complete(map[string]any{
|
||||
"result": mcpResult.Content,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -566,8 +566,9 @@ type Message struct {
|
|||
ToolCallID *string `json:"tool_call_id,omitempty"` // Required for tool messages: tool call that this message is responding to
|
||||
|
||||
// Assistant message specific fields
|
||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"` // Optional for assistant: tool calls generated by the model
|
||||
Refusal *string `json:"refusal,omitempty"` // Optional for assistant: refusal message (null when not refusing)
|
||||
ReasoningContent string `json:"reasoning_content,omitempty"` // Optional for assistant: reasoning/thinking content (DeepSeek, OpenAI o-series)
|
||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"` // Optional for assistant: tool calls generated by the model
|
||||
Refusal *string `json:"refusal,omitempty"` // Optional for assistant: refusal message (null when not refusing)
|
||||
}
|
||||
|
||||
// ContentPartType represents the type of content part
|
||||
|
|
|
|||
|
|
@ -1084,6 +1084,10 @@ func (p *Provider) buildRequestBody(messages []context.Message, options *context
|
|||
apiMsg["tool_calls"] = msg.ToolCalls
|
||||
}
|
||||
|
||||
if msg.ReasoningContent != "" {
|
||||
apiMsg["reasoning_content"] = msg.ReasoningContent
|
||||
}
|
||||
|
||||
if msg.Refusal != nil {
|
||||
apiMsg["refusal"] = *msg.Refusal
|
||||
}
|
||||
|
|
|
|||
812
data/bindata.go
812
data/bindata.go
File diff suppressed because it is too large
Load diff
|
|
@ -18,17 +18,14 @@ var allowedPrefixes = []string{
|
|||
"stores.",
|
||||
"flows.",
|
||||
"scripts.",
|
||||
"services.",
|
||||
"tasks.",
|
||||
"schedules.",
|
||||
"widgets.",
|
||||
"utils.",
|
||||
"http.",
|
||||
}
|
||||
|
||||
// Explicitly blocked prefixes for safety.
|
||||
var blockedPrefixes = []string{
|
||||
"yao.sys.",
|
||||
"yao.env.",
|
||||
"utils.",
|
||||
"tools.",
|
||||
}
|
||||
|
||||
|
|
|
|||
8
yao/assistants/loop_fallback/package.yao
Normal file
8
yao/assistants/loop_fallback/package.yao
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "Loop Fallback",
|
||||
"description": "Synthesize tool call results into natural language when tool loop fails or reaches max turns",
|
||||
"type": "worker",
|
||||
"connector": "use::light",
|
||||
"uses": { "search": "disabled" },
|
||||
"options": {}
|
||||
}
|
||||
40
yao/assistants/loop_fallback/prompts.yml
Normal file
40
yao/assistants/loop_fallback/prompts.yml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
- role: system
|
||||
content: |
|
||||
You are a response synthesizer for an AI assistant.
|
||||
You are the FALLBACK — you are called only when the primary tool loop has been exhausted.
|
||||
|
||||
## Your Input
|
||||
|
||||
You will receive a single message containing three sections in Markdown format:
|
||||
|
||||
### "## Assistant Context"
|
||||
This is the system prompt / personality of the original assistant that the user is talking to.
|
||||
You MUST adopt this context as your own — respond as if you ARE that assistant.
|
||||
Follow its tone, domain expertise, language preferences, and constraints.
|
||||
|
||||
### "## Conversation"
|
||||
This shows the recent conversation between the user and the assistant.
|
||||
The last user message is the question you need to answer.
|
||||
|
||||
### "## Tool Results"
|
||||
These are the results from tools that were executed to help answer the user's question.
|
||||
Each result is labeled with the tool name and contains JSON data or an error message.
|
||||
|
||||
## Your Task
|
||||
|
||||
1. Read the Assistant Context — adopt that persona
|
||||
2. Understand what the user asked in the Conversation
|
||||
3. Use the Tool Results data to formulate your answer
|
||||
4. Respond naturally as the original assistant would
|
||||
|
||||
## Rules
|
||||
|
||||
- Respond in the SAME LANGUAGE as the user's message
|
||||
- Be concise and directly answer what was asked
|
||||
- Do NOT mention tool names, server IDs, JSON structures, or any technical internals
|
||||
- Do NOT say "based on the tool results" or "according to the data" — just answer naturally
|
||||
- If results contain structured data (lists, tables), format them readably
|
||||
- If the Assistant Context has specific formatting/style rules, follow them
|
||||
- You have NO access to any tools — do NOT suggest calling tools, retrying, or trying alternative methods
|
||||
- Work ONLY with the Tool Results provided — do NOT fabricate or guess data
|
||||
- If all tool results are errors or empty, honestly tell the user the request could not be completed and suggest rephrasing
|
||||
Loading…
Add table
Reference in a new issue