- Introduced multiple executor modes (Standard, DryRun, Sandbox) to accommodate various use cases, enhancing flexibility in execution strategies. - Updated DESIGN.md to reflect the new executor modes and their respective use cases, including detailed descriptions and configuration examples. - Revised TECHNICAL.md to outline the new executor package structure, emphasizing the modular design for future enhancements. - Enhanced the TODO.md to track the progress of executor mode implementations and related tasks. - Removed outdated executor stub files and tests, streamlining the codebase for improved maintainability. - Updated integration tests to utilize the new DryRun executor, ensuring comprehensive coverage of execution scenarios without real agent calls.
435 lines
12 KiB
Go
435 lines
12 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
|
|
// - 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 *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()
|
|
}
|
|
|
|
// 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")
|
|
|
|
// 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 []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
|
|
validatedCount := 0
|
|
|
|
for _, result := range results {
|
|
if result.Success {
|
|
successCount++
|
|
} else {
|
|
failCount++
|
|
}
|
|
if result.Validation != nil && result.Validation.Passed {
|
|
validatedCount++
|
|
}
|
|
|
|
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\n",
|
|
len(results), successCount, failCount, validatedCount))
|
|
|
|
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,
|
|
},
|
|
}
|
|
}
|