package standard import ( "context" "encoding/json" "fmt" "strings" "github.com/yaoapp/gou/mcp" "github.com/yaoapp/yao/agent/assistant" agentcontext "github.com/yaoapp/yao/agent/context" "github.com/yaoapp/yao/agent/i18n" robottypes "github.com/yaoapp/yao/agent/robot/types" ) // InputFormatter provides methods to format input data for assistant prompts // Each phase has specific input requirements: // - P0 (Inspiration): ClockContext + Robot identity + Available resources // - P1 (Goals): InspirationReport/TriggerInput + Robot identity + Available resources // - P2 (Tasks): Goals + Available resources // - P3 (Run): Tasks // - P4 (Delivery): Task results // - P5 (Learning): Execution summary type InputFormatter struct{} // NewInputFormatter creates a new InputFormatter func NewInputFormatter() *InputFormatter { return &InputFormatter{} } // FormatClockContext formats ClockContext as user message content // Used by P0 (Inspiration) phase func (f *InputFormatter) FormatClockContext(clock *robottypes.ClockContext, robot *robottypes.Robot) string { if clock == nil { return "" } var sb strings.Builder // Time context section sb.WriteString("## Current Time Context\n\n") sb.WriteString(fmt.Sprintf("- **Now**: %s\n", clock.Now.Format("2006-01-02 15:04:05"))) sb.WriteString(fmt.Sprintf("- **Day**: %s\n", clock.DayOfWeek)) sb.WriteString(fmt.Sprintf("- **Date**: %d/%d/%d\n", clock.Year, clock.Month, clock.DayOfMonth)) sb.WriteString(fmt.Sprintf("- **Week**: %d of year\n", clock.WeekOfYear)) sb.WriteString(fmt.Sprintf("- **Hour**: %d\n", clock.Hour)) sb.WriteString(fmt.Sprintf("- **Timezone**: %s\n", clock.TZ)) // Time markers - show all markers with check/cross for context awareness sb.WriteString("\n### Time Markers\n") sb.WriteString(fmt.Sprintf("- %s Weekend\n", boolMark(clock.IsWeekend))) sb.WriteString(fmt.Sprintf("- %s Month Start (1st-3rd)\n", boolMark(clock.IsMonthStart))) sb.WriteString(fmt.Sprintf("- %s Month End (last 3 days)\n", boolMark(clock.IsMonthEnd))) sb.WriteString(fmt.Sprintf("- %s Quarter End\n", boolMark(clock.IsQuarterEnd))) sb.WriteString(fmt.Sprintf("- %s Year End\n", boolMark(clock.IsYearEnd))) // Robot identity section // Priority: Config.Identity > Robot fields (DisplayName, Bio, SystemPrompt) if robot != nil { if robot.Config != nil && robot.Config.Identity != nil { // Use structured identity from config sb.WriteString("\n## Robot Identity\n\n") sb.WriteString(fmt.Sprintf("- **Role**: %s\n", robot.Config.Identity.Role)) if len(robot.Config.Identity.Duties) > 0 { sb.WriteString("- **Duties**:\n") for _, duty := range robot.Config.Identity.Duties { sb.WriteString(fmt.Sprintf(" - %s\n", duty)) } } if len(robot.Config.Identity.Rules) > 0 { sb.WriteString("- **Rules**:\n") for _, rule := range robot.Config.Identity.Rules { sb.WriteString(fmt.Sprintf(" - %s\n", rule)) } } } else if robot.DisplayName != "" || robot.Bio != "" || robot.SystemPrompt != "" { // Fallback: build identity from Robot fields (from __yao.member table) sb.WriteString("\n## Robot Identity\n\n") if robot.DisplayName != "" { sb.WriteString(fmt.Sprintf("- **Role**: %s\n", robot.DisplayName)) } if robot.Bio != "" { sb.WriteString(fmt.Sprintf("- **Description**: %s\n", robot.Bio)) } if robot.SystemPrompt != "" { sb.WriteString(fmt.Sprintf("- **Instructions**:\n%s\n", robot.SystemPrompt)) } } } return sb.String() } // boolMark returns ✓ for true and ✗ for false func boolMark(v bool) string { if v { return "✓" } return "✗" } // FormatRobotIdentity formats robot identity as user message content // Used to provide context about the robot's role and duties func (f *InputFormatter) FormatRobotIdentity(robot *robottypes.Robot) string { if robot == nil || robot.Config == nil || robot.Config.Identity == nil { return "" } var sb strings.Builder identity := robot.Config.Identity sb.WriteString("## Robot Identity\n\n") sb.WriteString(fmt.Sprintf("- **Role**: %s\n", identity.Role)) if len(identity.Duties) > 0 { sb.WriteString("- **Duties**:\n") for _, duty := range identity.Duties { sb.WriteString(fmt.Sprintf(" - %s\n", duty)) } } if len(identity.Rules) > 0 { sb.WriteString("- **Rules**:\n") for _, rule := range identity.Rules { sb.WriteString(fmt.Sprintf(" - %s\n", rule)) } } return sb.String() } // FormatAvailableResources formats available resources (agents, MCP tools, KB, DB) as user message content // Used by P0 (Inspiration) and P1 (Goals) to inform the agent what tools are available // This is critical for generating achievable goals - without knowing available tools, // the agent might generate goals that cannot be accomplished func (f *InputFormatter) FormatAvailableResources(robot *robottypes.Robot) string { locale := "en" // default locale if robot != nil && robot.Config != nil { locale = robot.Config.GetDefaultLocale() } return f.FormatAvailableResourcesWithLocale(robot, locale) } // FormatAvailableResourcesWithLocale formats available resources with specific locale for i18n support func (f *InputFormatter) FormatAvailableResourcesWithLocale(robot *robottypes.Robot, locale string) string { if robot == nil || robot.Config == nil { return "" } var sb strings.Builder hasContent := false // Available Agents - with detailed information (name, description) if robot.Config.Resources != nil && len(robot.Config.Resources.Agents) > 0 { if !hasContent { sb.WriteString("## Available Resources\n\n") hasContent = true } sb.WriteString("### Agents\n") sb.WriteString("These are the AI assistants you can delegate tasks to:\n\n") for _, agentID := range robot.Config.Resources.Agents { // Try to get agent details ast, err := assistant.Get(agentID) if err != nil { // Fallback to just ID if agent not found sb.WriteString(fmt.Sprintf("- **%s**\n", agentID)) continue } // Get localized name and description name := i18n.Translate(agentID, locale, ast.Name).(string) description := "" if ast.Description != "" { description = i18n.Translate(agentID, locale, ast.Description).(string) } // Format agent info sb.WriteString(fmt.Sprintf("- **%s** (`%s`)\n", name, agentID)) if description != "" { sb.WriteString(fmt.Sprintf(" - %s\n", description)) } if ast.Capabilities != "" { capabilities := i18n.Translate(agentID, locale, ast.Capabilities).(string) sb.WriteString(fmt.Sprintf(" - **Capabilities**: %s\n", capabilities)) } } sb.WriteString("\n") } // Available MCP Tools - with detailed tool information if robot.Config.Resources != nil && len(robot.Config.Resources.MCP) > 0 { if !hasContent { sb.WriteString("## Available Resources\n\n") hasContent = true } sb.WriteString("### MCP Tools\n") sb.WriteString("These are the external tools and services you can use:\n\n") for _, mcpConfig := range robot.Config.Resources.MCP { // Try to get MCP client and list tools client, err := mcp.Select(mcpConfig.ID) if err != nil { // Fallback to basic info if client not found if len(mcpConfig.Tools) > 0 { sb.WriteString(fmt.Sprintf("- **%s**: %s\n", mcpConfig.ID, strings.Join(mcpConfig.Tools, ", "))) } else { sb.WriteString(fmt.Sprintf("- **%s**: all tools available\n", mcpConfig.ID)) } continue } // Get client info for name and description clientInfo := client.Info() clientName := mcpConfig.ID clientDesc := "" if clientInfo != nil { if clientInfo.Name != "" { clientName = clientInfo.Name } if clientInfo.Description != "" { clientDesc = clientInfo.Description } } // Write MCP header if clientDesc != "" { sb.WriteString(fmt.Sprintf("#### %s (`%s`)\n", clientName, mcpConfig.ID)) sb.WriteString(fmt.Sprintf("%s\n\n", clientDesc)) } else { sb.WriteString(fmt.Sprintf("#### %s (`%s`)\n\n", clientName, mcpConfig.ID)) } // Try to list tools with context ctx := context.Background() toolsResp, err := client.ListTools(ctx, "") if err != nil || toolsResp == nil { // Fallback to configured tools if len(mcpConfig.Tools) > 0 { sb.WriteString("Available tools: ") sb.WriteString(strings.Join(mcpConfig.Tools, ", ")) sb.WriteString("\n\n") } else { sb.WriteString("All tools available\n\n") } continue } // Filter tools if specific tools are configured toolsToShow := toolsResp.Tools if len(mcpConfig.Tools) > 0 { // Create a map for quick lookup allowedTools := make(map[string]bool) for _, t := range mcpConfig.Tools { allowedTools[t] = true } // Filter tools var filteredTools []struct { Name string Description string } for _, tool := range toolsResp.Tools { if allowedTools[tool.Name] { filteredTools = append(filteredTools, struct { Name string Description string }{tool.Name, tool.Description}) } } // Write filtered tools if len(filteredTools) > 0 { sb.WriteString("| Tool | Description |\n") sb.WriteString("|------|-------------|\n") for _, tool := range filteredTools { desc := tool.Description if len(desc) > 100 { desc = desc[:97] + "..." } // Escape pipe characters in description desc = strings.ReplaceAll(desc, "|", "\\|") sb.WriteString(fmt.Sprintf("| `%s` | %s |\n", tool.Name, desc)) } } else { sb.WriteString("Configured tools: ") sb.WriteString(strings.Join(mcpConfig.Tools, ", ")) } } else if len(toolsToShow) > 0 { // Show all available tools sb.WriteString("| Tool | Description |\n") sb.WriteString("|------|-------------|\n") for _, tool := range toolsToShow { desc := tool.Description if len(desc) > 100 { desc = desc[:97] + "..." } // Escape pipe characters in description desc = strings.ReplaceAll(desc, "|", "\\|") sb.WriteString(fmt.Sprintf("| `%s` | %s |\n", tool.Name, desc)) } } else { sb.WriteString("No tools available\n") } sb.WriteString("\n") } } // Available Knowledge Base if robot.Config.KB != nil && len(robot.Config.KB.Collections) > 0 { if !hasContent { sb.WriteString("## Available Resources\n\n") hasContent = true } sb.WriteString("### Knowledge Base\n") sb.WriteString("You have access to these knowledge collections:\n") for _, collection := range robot.Config.KB.Collections { sb.WriteString(fmt.Sprintf("- %s\n", collection)) } sb.WriteString("\n") } // Available Database Models if robot.Config.DB != nil && len(robot.Config.DB.Models) > 0 { if !hasContent { sb.WriteString("## Available Resources\n\n") hasContent = true } sb.WriteString("### Database\n") sb.WriteString("You can query these database models:\n") for _, model := range robot.Config.DB.Models { sb.WriteString(fmt.Sprintf("- %s\n", model)) } sb.WriteString("\n") } if !hasContent { return "" } sb.WriteString("**Important**: Only plan goals and tasks that can be accomplished with the above resources.\n") return sb.String() } // FormatInspirationReport formats InspirationReport as user message content // Used by P1 (Goals) phase when trigger is Clock func (f *InputFormatter) FormatInspirationReport(report *robottypes.InspirationReport) string { if report == nil { return "" } var sb strings.Builder // Clock context summary (if available) if report.Clock != nil { sb.WriteString("## Time Context\n\n") sb.WriteString(fmt.Sprintf("- **Time**: %s %s\n", report.Clock.DayOfWeek, report.Clock.Now.Format("15:04"))) sb.WriteString(fmt.Sprintf("- **Date**: %d/%d/%d\n", report.Clock.Year, report.Clock.Month, report.Clock.DayOfMonth)) // Add relevant time markers var markers []string if report.Clock.IsWeekend { markers = append(markers, "Weekend") } if report.Clock.IsMonthStart { markers = append(markers, "Month Start") } if report.Clock.IsMonthEnd { markers = append(markers, "Month End") } if report.Clock.IsQuarterEnd { markers = append(markers, "Quarter End") } if len(markers) > 0 { sb.WriteString(fmt.Sprintf("- **Markers**: %s\n", strings.Join(markers, ", "))) } sb.WriteString("\n") } // Inspiration content if report.Content != "" { sb.WriteString("## Inspiration Report\n\n") sb.WriteString(report.Content) sb.WriteString("\n") } return sb.String() } // FormatTriggerInput formats TriggerInput as user message content // Used by P1 (Goals) phase when trigger is Human or Event func (f *InputFormatter) FormatTriggerInput(input *robottypes.TriggerInput) string { if input == nil { return "" } var sb strings.Builder // Human intervention if input.Action != "" { sb.WriteString("## Human Intervention\n\n") sb.WriteString(fmt.Sprintf("- **Action**: %s\n", input.Action)) if input.UserID != "" { sb.WriteString(fmt.Sprintf("- **User**: %s\n", input.UserID)) } // Messages if len(input.Messages) > 0 { sb.WriteString("\n### User Input\n\n") for _, msg := range input.Messages { if content, ok := msg.Content.(string); ok { sb.WriteString(content) sb.WriteString("\n") } } } return sb.String() } // Event trigger if input.Source != "" { sb.WriteString("## Event Trigger\n\n") sb.WriteString(fmt.Sprintf("- **Source**: %s\n", input.Source)) sb.WriteString(fmt.Sprintf("- **Event Type**: %s\n", input.EventType)) // Event data if input.Data != nil { sb.WriteString("\n### Event Data\n\n") sb.WriteString("```json\n") if data, err := json.MarshalIndent(input.Data, "", " "); err == nil { sb.WriteString(string(data)) } sb.WriteString("\n```\n") } return sb.String() } return "" } // FormatGoals formats Goals as user message content // Used by P2 (Tasks) phase func (f *InputFormatter) FormatGoals(goals *robottypes.Goals, robot *robottypes.Robot) string { if goals == nil { return "" } var sb strings.Builder // Goals content sb.WriteString("## Goals\n\n") sb.WriteString(goals.Content) sb.WriteString("\n") // Delivery target (from P1) - important for task planning // Tasks should be designed to produce output suitable for the delivery method if goals.Delivery != nil { sb.WriteString("\n## Delivery Target\n\n") sb.WriteString(fmt.Sprintf("- **Type**: %s\n", goals.Delivery.Type)) if len(goals.Delivery.Recipients) > 0 { sb.WriteString(fmt.Sprintf("- **Recipients**: %s\n", strings.Join(goals.Delivery.Recipients, ", "))) } if goals.Delivery.Format != "" { sb.WriteString(fmt.Sprintf("- **Format**: %s\n", goals.Delivery.Format)) } if goals.Delivery.Template != "" { sb.WriteString(fmt.Sprintf("- **Template**: %s\n", goals.Delivery.Template)) } sb.WriteString("\n**Note**: Design tasks to produce output suitable for this delivery method.\n") } // Available resources - reuse FormatAvailableResources for consistency resourcesContent := f.FormatAvailableResources(robot) if resourcesContent != "" { sb.WriteString("\n") sb.WriteString(resourcesContent) } return sb.String() } // FormatTasks formats Tasks as user message content // Used by P3 (Run) phase func (f *InputFormatter) FormatTasks(tasks []robottypes.Task) string { if len(tasks) == 0 { return "No tasks to execute." } var sb strings.Builder sb.WriteString("## Tasks to Execute\n\n") for i, task := range tasks { sb.WriteString(fmt.Sprintf("### Task %d: %s\n\n", i+1, task.ID)) sb.WriteString(fmt.Sprintf("- **Goal Reference**: %s\n", task.GoalRef)) sb.WriteString(fmt.Sprintf("- **Source**: %s\n", task.Source)) sb.WriteString(fmt.Sprintf("- **Executor**: %s (%s)\n", task.ExecutorID, task.ExecutorType)) // Task content if len(task.Messages) > 0 { sb.WriteString("\n**Instructions**:\n") for _, msg := range task.Messages { if content, ok := msg.Content.(string); ok { sb.WriteString(content) sb.WriteString("\n") } } } // Arguments if len(task.Args) > 0 { sb.WriteString("\n**Arguments**:\n") if args, err := json.MarshalIndent(task.Args, "", " "); err == nil { sb.WriteString("```json\n") sb.WriteString(string(args)) sb.WriteString("\n```\n") } } sb.WriteString("\n") } return sb.String() } // FormatTaskResults formats TaskResults as user message content // Used by P4 (Delivery) and P5 (Learning) phases func (f *InputFormatter) FormatTaskResults(results []robottypes.TaskResult) string { if len(results) == 0 { return "No task results." } var sb strings.Builder sb.WriteString("## Task Results\n\n") successCount := 0 failCount := 0 validatedPassedCount := 0 validatedTotalCount := 0 for _, result := range results { if result.Success { successCount++ } else { failCount++ } if result.Validation != nil { validatedTotalCount++ if result.Validation.Passed { validatedPassedCount++ } } sb.WriteString(fmt.Sprintf("### Task: %s\n\n", result.TaskID)) if result.Success { sb.WriteString("- **Status**: ✓ Success\n") } else { sb.WriteString("- **Status**: ✗ Failed\n") } sb.WriteString(fmt.Sprintf("- **Duration**: %dms\n", result.Duration)) // Validation result (P3) if result.Validation != nil { if result.Validation.Passed { sb.WriteString(fmt.Sprintf("- **Validation**: ✓ Passed (score: %.2f)\n", result.Validation.Score)) } else { sb.WriteString("- **Validation**: ✗ Failed\n") if len(result.Validation.Issues) > 0 { sb.WriteString(" - Issues:\n") for _, issue := range result.Validation.Issues { sb.WriteString(fmt.Sprintf(" - %s\n", issue)) } } } } // Output if result.Output != nil { sb.WriteString("\n**Output**:\n") if output, err := json.MarshalIndent(result.Output, "", " "); err == nil { sb.WriteString("```json\n") sb.WriteString(string(output)) sb.WriteString("\n```\n") } else { sb.WriteString(fmt.Sprintf("%v\n", result.Output)) } } // Error if result.Error != "" { sb.WriteString(fmt.Sprintf("\n**Error**: %s\n", result.Error)) } sb.WriteString("\n") } // Summary sb.WriteString(fmt.Sprintf("## Summary\n\n- Total: %d tasks\n- Success: %d\n- Failed: %d\n- Validated: %d/%d\n", len(results), successCount, failCount, validatedPassedCount, validatedTotalCount)) return sb.String() } // FormatExecutionSummary formats the entire execution for P5 (Learning) phase func (f *InputFormatter) FormatExecutionSummary(exec *robottypes.Execution) string { if exec == nil { return "" } var sb strings.Builder // Execution metadata sb.WriteString("## Execution Summary\n\n") sb.WriteString(fmt.Sprintf("- **ID**: %s\n", exec.ID)) sb.WriteString(fmt.Sprintf("- **Trigger**: %s\n", exec.TriggerType)) sb.WriteString(fmt.Sprintf("- **Status**: %s\n", exec.Status)) sb.WriteString(fmt.Sprintf("- **Start Time**: %s\n", exec.StartTime.Format("2006-01-02 15:04:05"))) if exec.EndTime != nil { sb.WriteString(fmt.Sprintf("- **End Time**: %s\n", exec.EndTime.Format("2006-01-02 15:04:05"))) duration := exec.EndTime.Sub(exec.StartTime) sb.WriteString(fmt.Sprintf("- **Duration**: %s\n", duration.String())) } if exec.Error != "" { sb.WriteString(fmt.Sprintf("- **Error**: %s\n", exec.Error)) } sb.WriteString("\n") // Inspiration (P0) if exec.Inspiration != nil && exec.Inspiration.Content != "" { sb.WriteString("## Inspiration (P0)\n\n") sb.WriteString(exec.Inspiration.Content) sb.WriteString("\n\n") } // Goals (P1) if exec.Goals != nil && exec.Goals.Content != "" { sb.WriteString("## Goals (P1)\n\n") sb.WriteString(exec.Goals.Content) sb.WriteString("\n\n") } // Tasks (P2) if len(exec.Tasks) > 0 { sb.WriteString("## Tasks (P2)\n\n") for i, task := range exec.Tasks { sb.WriteString(fmt.Sprintf("%d. [%s] %s (executor: %s)\n", i+1, task.Status, task.ID, task.ExecutorID)) } sb.WriteString("\n") } // Results (P3) if len(exec.Results) > 0 { sb.WriteString("## Results (P3)\n\n") for _, result := range exec.Results { status := "✓" if !result.Success { status = "✗" } sb.WriteString(fmt.Sprintf("- %s %s (%dms)\n", status, result.TaskID, result.Duration)) } sb.WriteString("\n") } // Delivery (P4) if exec.Delivery != nil { sb.WriteString("## Delivery (P4)\n\n") if exec.Delivery.Content != nil { sb.WriteString(fmt.Sprintf("- **Summary**: %s\n", exec.Delivery.Content.Summary)) } if exec.Delivery.Success { sb.WriteString("- **Status**: ✓ Success\n") } else { sb.WriteString(fmt.Sprintf("- **Status**: ✗ Failed (%s)\n", exec.Delivery.Error)) } if len(exec.Delivery.Results) > 0 { sb.WriteString(fmt.Sprintf("- **Channels**: %d\n", len(exec.Delivery.Results))) } sb.WriteString("\n") } return sb.String() } // BuildMessages is a convenience method to build messages array from content func (f *InputFormatter) BuildMessages(userContent string) []agentcontext.Message { return []agentcontext.Message{ { Role: agentcontext.RoleUser, Content: userContent, }, } } // BuildMessagesWithSystem builds messages array with system and user content func (f *InputFormatter) BuildMessagesWithSystem(systemContent, userContent string) []agentcontext.Message { return []agentcontext.Message{ { Role: agentcontext.RoleSystem, Content: systemContent, }, { Role: agentcontext.RoleUser, Content: userContent, }, } }