yao/agent/robot/executor/standard/input.go
Max 4bfee3b39e Enhance P1 and P2 Implementation in Robot Agent
- Marked P1 Goals and P2 Tasks as completed in TODO.md, reflecting the successful implementation of goal generation and task planning functionalities.
- Updated the input formatter to include delivery target details in the goal output, ensuring tasks are designed for appropriate delivery methods.
- Enhanced the RunTasks method to validate goals and parse tasks from agent responses, including comprehensive error handling and task validation.
- Added unit tests for new task parsing and validation features, ensuring robust coverage of task generation and execution scenarios.
- Revised documentation to clarify the integration of validation rules and expected outputs in task management.
2026-01-17 11:01:56 +08:00

548 lines
16 KiB
Go

package standard
import (
"encoding/json"
"fmt"
"strings"
agentcontext "github.com/yaoapp/yao/agent/context"
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("- **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()
}
// 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 {
if robot == nil || robot.Config == nil {
return ""
}
var sb strings.Builder
hasContent := false
// Available Agents
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")
for _, agent := range robot.Config.Resources.Agents {
sb.WriteString(fmt.Sprintf("- **%s**\n", agent))
}
sb.WriteString("\n")
}
// Available MCP Tools
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")
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 available\n", mcp.ID))
}
}
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")
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,
},
}
}