yao/agent/robot/executor/input.go
Max bcb04f8677 Implement Phase 4: Agent Call Infrastructure and Update TODO.md
- Established a unified calling mechanism for agents, enabling streaming support and multi-turn conversations.
- Developed input formatters for various phases, ensuring proper data preparation for assistant prompts.
- Created test assistants for single and multi-turn interactions, along with comprehensive test cases for the AgentCaller and InputFormatter.
- Updated the TODO.md to reflect the new structure and progress of the agent call infrastructure, including future phases for assistant setup and implementation.
2026-01-16 09:52:57 +08:00

417 lines
11 KiB
Go

package executor
import (
"encoding/json"
"fmt"
"strings"
agentcontext "github.com/yaoapp/yao/agent/context"
"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
// - P1 (Goals): InspirationReport (Clock) or TriggerInput (Human/Event)
// - P2 (Tasks): Goals + Available tools
// - 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 *types.ClockContext, robot *types.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("- **Timezone**: %s\n", clock.TZ))
// Time markers
sb.WriteString("\n### Time Markers\n")
if clock.IsWeekend {
sb.WriteString("- ✓ Weekend\n")
}
if clock.IsMonthStart {
sb.WriteString("- ✓ Month Start (1st-3rd)\n")
}
if clock.IsMonthEnd {
sb.WriteString("- ✓ Month End (last 3 days)\n")
}
if clock.IsQuarterEnd {
sb.WriteString("- ✓ Quarter End\n")
}
if clock.IsYearEnd {
sb.WriteString("- ✓ Year End\n")
}
// Robot identity section (if available)
if robot != nil && robot.Config != nil && robot.Config.Identity != nil {
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))
}
}
}
return sb.String()
}
// FormatInspirationReport formats InspirationReport as user message content
// Used by P1 (Goals) phase when trigger is Clock
func (f *InputFormatter) FormatInspirationReport(report *types.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 *types.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 *types.Goals, robot *types.Robot) string {
if goals == nil {
return ""
}
var sb strings.Builder
// Goals content
sb.WriteString("## Goals\n\n")
sb.WriteString(goals.Content)
sb.WriteString("\n")
// Available resources (if robot config available)
if robot != nil && robot.Config != nil && robot.Config.Resources != nil {
sb.WriteString("\n## Available Resources\n\n")
// Agents
if len(robot.Config.Resources.Agents) > 0 {
sb.WriteString("### Agents\n")
for _, agent := range robot.Config.Resources.Agents {
sb.WriteString(fmt.Sprintf("- %s\n", agent))
}
}
// MCP tools
if len(robot.Config.Resources.MCP) > 0 {
sb.WriteString("\n### MCP Tools\n")
for _, mcp := range robot.Config.Resources.MCP {
if len(mcp.Tools) > 0 {
sb.WriteString(fmt.Sprintf("- %s: %s\n", mcp.ID, strings.Join(mcp.Tools, ", ")))
} else {
sb.WriteString(fmt.Sprintf("- %s: all tools\n", mcp.ID))
}
}
}
}
return sb.String()
}
// FormatTasks formats Tasks as user message content
// Used by P3 (Run) phase
func (f *InputFormatter) FormatTasks(tasks []types.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 []types.TaskResult) string {
if len(results) == 0 {
return "No task results."
}
var sb strings.Builder
sb.WriteString("## Task Results\n\n")
successCount := 0
failCount := 0
for _, result := range results {
if result.Success {
successCount++
} else {
failCount++
}
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))
sb.WriteString(fmt.Sprintf("- **Validated**: %t\n", result.Validated))
// 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",
len(results), successCount, failCount))
return sb.String()
}
// FormatExecutionSummary formats the entire execution for P5 (Learning) phase
func (f *InputFormatter) FormatExecutionSummary(exec *types.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")
sb.WriteString(fmt.Sprintf("- **Type**: %s\n", exec.Delivery.Type))
if exec.Delivery.Success {
sb.WriteString("- **Status**: ✓ Success\n")
} else {
sb.WriteString(fmt.Sprintf("- **Status**: ✗ Failed (%s)\n", exec.Delivery.Error))
}
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,
},
}
}