- 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.
417 lines
11 KiB
Go
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,
|
|
},
|
|
}
|
|
}
|