Enhance Input Formatting and Error Handling in Robot Executor
- Introduced a new method to format available resources, improving clarity on the tools and agents accessible to the robot. - Updated the RunGoals and RunInspiration methods to include resource information in user prompts, ensuring achievable goals and insights. - Revised error messages in the executor to provide more context, enhancing debugging capabilities. - Added comprehensive tests for the new resource formatting functionality and updated existing tests to reflect changes in error handling. - Improved documentation for robot configuration, emphasizing the importance of available resources in goal and task generation.
This commit is contained in:
parent
2d86c9caad
commit
0c2123bb30
6 changed files with 359 additions and 52 deletions
|
|
@ -62,6 +62,13 @@ func (e *Executor) RunGoals(ctx *robottypes.Context, exec *robottypes.Execution,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add available resources - critical for generating achievable goals
|
||||||
|
// Without knowing what tools are available, goals might be unachievable
|
||||||
|
resourcesContent := formatter.FormatAvailableResources(robot)
|
||||||
|
if resourcesContent != "" {
|
||||||
|
userContent += "\n\n" + resourcesContent
|
||||||
|
}
|
||||||
|
|
||||||
if userContent == "" {
|
if userContent == "" {
|
||||||
return fmt.Errorf("no input available for goals generation")
|
return fmt.Errorf("no input available for goals generation")
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +77,7 @@ func (e *Executor) RunGoals(ctx *robottypes.Context, exec *robottypes.Execution,
|
||||||
caller := NewAgentCaller()
|
caller := NewAgentCaller()
|
||||||
result, err := caller.CallWithMessages(ctx, agentID, userContent)
|
result, err := caller.CallWithMessages(ctx, agentID, userContent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("goals agent call failed: %w", err)
|
return fmt.Errorf("goals agent (%s) call failed: %w", agentID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse response as JSON
|
// Parse response as JSON
|
||||||
|
|
@ -98,36 +105,38 @@ func (e *Executor) RunGoals(ctx *robottypes.Context, exec *robottypes.Execution,
|
||||||
|
|
||||||
// Extract delivery
|
// Extract delivery
|
||||||
if delivery, ok := data["delivery"].(map[string]interface{}); ok {
|
if delivery, ok := data["delivery"].(map[string]interface{}); ok {
|
||||||
exec.Goals.Delivery = parseDelivery(delivery)
|
exec.Goals.Delivery = ParseDelivery(delivery)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate: content is required
|
// Validate: content is required
|
||||||
if exec.Goals.Content == "" {
|
if exec.Goals.Content == "" {
|
||||||
return fmt.Errorf("goals agent returned empty content")
|
return fmt.Errorf("goals agent (%s) returned empty content", agentID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseDelivery converts map to DeliveryTarget struct
|
// ParseDelivery converts map to DeliveryTarget struct
|
||||||
func parseDelivery(data map[string]interface{}) *robottypes.DeliveryTarget {
|
// Returns nil if data is nil or type is invalid/missing
|
||||||
|
func ParseDelivery(data map[string]interface{}) *robottypes.DeliveryTarget {
|
||||||
if data == nil {
|
if data == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
target := &robottypes.DeliveryTarget{}
|
// Type is required - if missing or invalid, return nil
|
||||||
|
t, ok := data["type"].(string)
|
||||||
|
if !ok || t == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Parse and validate type
|
deliveryType := robottypes.DeliveryType(t)
|
||||||
if t, ok := data["type"].(string); ok {
|
if !IsValidDeliveryType(deliveryType) {
|
||||||
deliveryType := robottypes.DeliveryType(t)
|
// Invalid type - return nil to indicate parsing failure
|
||||||
switch deliveryType {
|
return nil
|
||||||
case robottypes.DeliveryEmail, robottypes.DeliveryWebhook,
|
}
|
||||||
robottypes.DeliveryFile, robottypes.DeliveryNotify:
|
|
||||||
target.Type = deliveryType
|
target := &robottypes.DeliveryTarget{
|
||||||
default:
|
Type: deliveryType,
|
||||||
// Invalid type - still set it but caller should validate
|
|
||||||
target.Type = deliveryType
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse recipients
|
// Parse recipients
|
||||||
|
|
|
||||||
|
|
@ -241,7 +241,7 @@ func TestRunGoalsErrorHandling(t *testing.T) {
|
||||||
err := e.RunGoals(ctx, exec, nil)
|
err := e.RunGoals(ctx, exec, nil)
|
||||||
|
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "agent call failed")
|
assert.Contains(t, err.Error(), "call failed")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("returns error when no input available and no identity", func(t *testing.T) {
|
t.Run("returns error when no input available and no identity", func(t *testing.T) {
|
||||||
|
|
@ -377,6 +377,105 @@ func TestDeliveryTypeValidation(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseDelivery(t *testing.T) {
|
||||||
|
t.Run("parses valid delivery with all fields", func(t *testing.T) {
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"type": "email",
|
||||||
|
"recipients": []interface{}{"user@example.com", "team@example.com"},
|
||||||
|
"format": "markdown",
|
||||||
|
"template": "weekly-report",
|
||||||
|
"options": map[string]interface{}{
|
||||||
|
"subject": "Weekly Report",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := standard.ParseDelivery(data)
|
||||||
|
|
||||||
|
require.NotNil(t, result)
|
||||||
|
assert.Equal(t, types.DeliveryEmail, result.Type)
|
||||||
|
assert.Equal(t, []string{"user@example.com", "team@example.com"}, result.Recipients)
|
||||||
|
assert.Equal(t, "markdown", result.Format)
|
||||||
|
assert.Equal(t, "weekly-report", result.Template)
|
||||||
|
assert.Equal(t, "Weekly Report", result.Options["subject"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns nil for nil data", func(t *testing.T) {
|
||||||
|
result := standard.ParseDelivery(nil)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns nil for missing type", func(t *testing.T) {
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"recipients": []interface{}{"user@example.com"},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := standard.ParseDelivery(data)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns nil for empty type", func(t *testing.T) {
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"type": "",
|
||||||
|
"recipients": []interface{}{"user@example.com"},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := standard.ParseDelivery(data)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns nil for invalid type", func(t *testing.T) {
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"type": "sms",
|
||||||
|
"recipients": []interface{}{"user@example.com"},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := standard.ParseDelivery(data)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles missing optional fields", func(t *testing.T) {
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"type": "webhook",
|
||||||
|
}
|
||||||
|
|
||||||
|
result := standard.ParseDelivery(data)
|
||||||
|
|
||||||
|
require.NotNil(t, result)
|
||||||
|
assert.Equal(t, types.DeliveryWebhook, result.Type)
|
||||||
|
assert.Empty(t, result.Recipients)
|
||||||
|
assert.Empty(t, result.Format)
|
||||||
|
assert.Empty(t, result.Template)
|
||||||
|
assert.Nil(t, result.Options)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles non-string recipients gracefully", func(t *testing.T) {
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"type": "email",
|
||||||
|
"recipients": []interface{}{"valid@example.com", 123, nil, "another@example.com"},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := standard.ParseDelivery(data)
|
||||||
|
|
||||||
|
require.NotNil(t, result)
|
||||||
|
// Only string recipients should be included
|
||||||
|
assert.Equal(t, []string{"valid@example.com", "another@example.com"}, result.Recipients)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("parses all valid delivery types", func(t *testing.T) {
|
||||||
|
validTypes := []string{"email", "webhook", "file", "notify"}
|
||||||
|
|
||||||
|
for _, dt := range validTypes {
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"type": dt,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := standard.ParseDelivery(data)
|
||||||
|
require.NotNil(t, result, "should parse type: %s", dt)
|
||||||
|
assert.Equal(t, types.DeliveryType(dt), result.Type)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// InputFormatter Tests for P1
|
// InputFormatter Tests for P1
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -450,6 +549,10 @@ func TestInputFormatterFormatRobotIdentity(t *testing.T) {
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// createGoalsTestRobot creates a test robot with specified goals agent
|
// createGoalsTestRobot creates a test robot with specified goals agent
|
||||||
|
// Includes available expert agents so the Goals Agent knows what resources are available
|
||||||
|
//
|
||||||
|
// Note: The agent IDs listed in Resources.Agents must exist in yao-dev-app/assistants/experts/
|
||||||
|
// Current available experts: data-analyst, summarizer, text-writer, web-reader
|
||||||
func createGoalsTestRobot(t *testing.T, agentID string) *types.Robot {
|
func createGoalsTestRobot(t *testing.T, agentID string) *types.Robot {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
return &types.Robot{
|
return &types.Robot{
|
||||||
|
|
@ -459,12 +562,24 @@ func createGoalsTestRobot(t *testing.T, agentID string) *types.Robot {
|
||||||
Config: &types.Config{
|
Config: &types.Config{
|
||||||
Identity: &types.Identity{
|
Identity: &types.Identity{
|
||||||
Role: "Test Assistant",
|
Role: "Test Assistant",
|
||||||
Duties: []string{"Testing", "Validation"},
|
Duties: []string{"Testing", "Validation", "Data Analysis", "Report Generation"},
|
||||||
},
|
},
|
||||||
Resources: &types.Resources{
|
Resources: &types.Resources{
|
||||||
Phases: map[types.Phase]string{
|
Phases: map[types.Phase]string{
|
||||||
types.PhaseGoals: agentID,
|
types.PhaseGoals: agentID,
|
||||||
},
|
},
|
||||||
|
// Available expert agents that can be delegated to
|
||||||
|
// These IDs correspond to assistants in yao-dev-app/assistants/experts/
|
||||||
|
Agents: []string{
|
||||||
|
"experts.data-analyst", // Data analysis and insights
|
||||||
|
"experts.summarizer", // Content summarization
|
||||||
|
"experts.text-writer", // Report and document generation
|
||||||
|
"experts.web-reader", // Web content extraction
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Knowledge base collections (if any)
|
||||||
|
KB: &types.KB{
|
||||||
|
Collections: []string{"test-knowledge"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@ import (
|
||||||
|
|
||||||
// InputFormatter provides methods to format input data for assistant prompts
|
// InputFormatter provides methods to format input data for assistant prompts
|
||||||
// Each phase has specific input requirements:
|
// Each phase has specific input requirements:
|
||||||
// - P0 (Inspiration): ClockContext + Robot identity
|
// - P0 (Inspiration): ClockContext + Robot identity + Available resources
|
||||||
// - P1 (Goals): InspirationReport (Clock) or TriggerInput (Human/Event)
|
// - P1 (Goals): InspirationReport/TriggerInput + Robot identity + Available resources
|
||||||
// - P2 (Tasks): Goals + Available tools
|
// - P2 (Tasks): Goals + Available resources
|
||||||
// - P3 (Run): Tasks
|
// - P3 (Run): Tasks
|
||||||
// - P4 (Delivery): Task results
|
// - P4 (Delivery): Task results
|
||||||
// - P5 (Learning): Execution summary
|
// - P5 (Learning): Execution summary
|
||||||
|
|
@ -110,6 +110,86 @@ func (f *InputFormatter) FormatRobotIdentity(robot *robottypes.Robot) string {
|
||||||
return sb.String()
|
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
|
// FormatInspirationReport formats InspirationReport as user message content
|
||||||
// Used by P1 (Goals) phase when trigger is Clock
|
// Used by P1 (Goals) phase when trigger is Clock
|
||||||
func (f *InputFormatter) FormatInspirationReport(report *robottypes.InspirationReport) string {
|
func (f *InputFormatter) FormatInspirationReport(report *robottypes.InspirationReport) string {
|
||||||
|
|
@ -220,29 +300,11 @@ func (f *InputFormatter) FormatGoals(goals *robottypes.Goals, robot *robottypes.
|
||||||
sb.WriteString(goals.Content)
|
sb.WriteString(goals.Content)
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
|
|
||||||
// Available resources (if robot config available)
|
// Available resources - reuse FormatAvailableResources for consistency
|
||||||
if robot != nil && robot.Config != nil && robot.Config.Resources != nil {
|
resourcesContent := f.FormatAvailableResources(robot)
|
||||||
sb.WriteString("\n## Available Resources\n\n")
|
if resourcesContent != "" {
|
||||||
|
sb.WriteString("\n")
|
||||||
// Agents
|
sb.WriteString(resourcesContent)
|
||||||
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()
|
return sb.String()
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,103 @@ func TestInputFormatterFormatInspirationReport(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInputFormatterFormatAvailableResources(t *testing.T) {
|
||||||
|
formatter := standard.NewInputFormatter()
|
||||||
|
|
||||||
|
t.Run("formats all resource types", func(t *testing.T) {
|
||||||
|
robot := &types.Robot{
|
||||||
|
MemberID: "test-robot",
|
||||||
|
Config: &types.Config{
|
||||||
|
Resources: &types.Resources{
|
||||||
|
Agents: []string{"data-analyst", "chart-gen", "report-writer"},
|
||||||
|
MCP: []types.MCPConfig{
|
||||||
|
{ID: "database", Tools: []string{"query", "insert"}},
|
||||||
|
{ID: "email", Tools: []string{}}, // all tools
|
||||||
|
},
|
||||||
|
},
|
||||||
|
KB: &types.KB{
|
||||||
|
Collections: []string{"sales-policies", "products"},
|
||||||
|
},
|
||||||
|
DB: &types.DB{
|
||||||
|
Models: []string{"sales", "customers", "orders"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := formatter.FormatAvailableResources(robot)
|
||||||
|
|
||||||
|
// Check structure
|
||||||
|
assert.Contains(t, result, "## Available Resources")
|
||||||
|
|
||||||
|
// Check agents
|
||||||
|
assert.Contains(t, result, "### Agents")
|
||||||
|
assert.Contains(t, result, "data-analyst")
|
||||||
|
assert.Contains(t, result, "chart-gen")
|
||||||
|
assert.Contains(t, result, "report-writer")
|
||||||
|
|
||||||
|
// Check MCP tools
|
||||||
|
assert.Contains(t, result, "### MCP Tools")
|
||||||
|
assert.Contains(t, result, "database")
|
||||||
|
assert.Contains(t, result, "query, insert")
|
||||||
|
assert.Contains(t, result, "email")
|
||||||
|
assert.Contains(t, result, "all tools available")
|
||||||
|
|
||||||
|
// Check KB
|
||||||
|
assert.Contains(t, result, "### Knowledge Base")
|
||||||
|
assert.Contains(t, result, "sales-policies")
|
||||||
|
assert.Contains(t, result, "products")
|
||||||
|
|
||||||
|
// Check DB
|
||||||
|
assert.Contains(t, result, "### Database")
|
||||||
|
assert.Contains(t, result, "sales")
|
||||||
|
assert.Contains(t, result, "customers")
|
||||||
|
assert.Contains(t, result, "orders")
|
||||||
|
|
||||||
|
// Check important note
|
||||||
|
assert.Contains(t, result, "Only plan goals and tasks that can be accomplished")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns empty for nil robot", func(t *testing.T) {
|
||||||
|
result := formatter.FormatAvailableResources(nil)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns empty for robot without config", func(t *testing.T) {
|
||||||
|
robot := &types.Robot{MemberID: "test"}
|
||||||
|
result := formatter.FormatAvailableResources(robot)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns empty for robot without resources", func(t *testing.T) {
|
||||||
|
robot := &types.Robot{
|
||||||
|
MemberID: "test",
|
||||||
|
Config: &types.Config{},
|
||||||
|
}
|
||||||
|
result := formatter.FormatAvailableResources(robot)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles partial resources", func(t *testing.T) {
|
||||||
|
robot := &types.Robot{
|
||||||
|
MemberID: "test",
|
||||||
|
Config: &types.Config{
|
||||||
|
Resources: &types.Resources{
|
||||||
|
Agents: []string{"single-agent"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := formatter.FormatAvailableResources(robot)
|
||||||
|
|
||||||
|
assert.Contains(t, result, "## Available Resources")
|
||||||
|
assert.Contains(t, result, "### Agents")
|
||||||
|
assert.Contains(t, result, "single-agent")
|
||||||
|
assert.NotContains(t, result, "### MCP Tools")
|
||||||
|
assert.NotContains(t, result, "### Knowledge Base")
|
||||||
|
assert.NotContains(t, result, "### Database")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestInputFormatterFormatTriggerInput(t *testing.T) {
|
func TestInputFormatterFormatTriggerInput(t *testing.T) {
|
||||||
formatter := standard.NewInputFormatter()
|
formatter := standard.NewInputFormatter()
|
||||||
|
|
||||||
|
|
@ -207,8 +304,10 @@ func TestInputFormatterFormatGoals(t *testing.T) {
|
||||||
assert.Contains(t, result, "### Agents")
|
assert.Contains(t, result, "### Agents")
|
||||||
assert.Contains(t, result, "data-analyzer")
|
assert.Contains(t, result, "data-analyzer")
|
||||||
assert.Contains(t, result, "### MCP Tools")
|
assert.Contains(t, result, "### MCP Tools")
|
||||||
assert.Contains(t, result, "database: query, insert")
|
assert.Contains(t, result, "database")
|
||||||
assert.Contains(t, result, "email: all tools")
|
assert.Contains(t, result, "query, insert")
|
||||||
|
assert.Contains(t, result, "email")
|
||||||
|
assert.Contains(t, result, "all tools available")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("formats goals without robot", func(t *testing.T) {
|
t.Run("formats goals without robot", func(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -41,17 +41,23 @@ func (e *Executor) RunInspiration(ctx *robottypes.Context, exec *robottypes.Exec
|
||||||
formatter := NewInputFormatter()
|
formatter := NewInputFormatter()
|
||||||
userContent := formatter.FormatClockContext(clock, robot)
|
userContent := formatter.FormatClockContext(clock, robot)
|
||||||
|
|
||||||
|
// Add available resources - critical for generating achievable insights
|
||||||
|
resourcesContent := formatter.FormatAvailableResources(robot)
|
||||||
|
if resourcesContent != "" {
|
||||||
|
userContent += "\n\n" + resourcesContent
|
||||||
|
}
|
||||||
|
|
||||||
// Call agent
|
// Call agent
|
||||||
caller := NewAgentCaller()
|
caller := NewAgentCaller()
|
||||||
result, err := caller.CallWithMessages(ctx, agentID, userContent)
|
result, err := caller.CallWithMessages(ctx, agentID, userContent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("inspiration agent call failed: %w", err)
|
return fmt.Errorf("inspiration agent (%s) call failed: %w", agentID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse response - get markdown content
|
// Parse response - get markdown content
|
||||||
content := result.GetText()
|
content := result.GetText()
|
||||||
if content == "" {
|
if content == "" {
|
||||||
return fmt.Errorf("inspiration agent returned empty response")
|
return fmt.Errorf("inspiration agent (%s) returned empty response", agentID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build InspirationReport
|
// Build InspirationReport
|
||||||
|
|
|
||||||
|
|
@ -201,7 +201,7 @@ func TestRunInspirationErrorHandling(t *testing.T) {
|
||||||
|
|
||||||
// Real AgentCaller returns error for non-existent agent
|
// Real AgentCaller returns error for non-existent agent
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "agent call failed")
|
assert.Contains(t, err.Error(), "call failed")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,7 +233,7 @@ func TestRunInspirationWithDefaultAgent(t *testing.T) {
|
||||||
// In test environment, we expect it to fail with "agent not found"
|
// In test environment, we expect it to fail with "agent not found"
|
||||||
// In production, it would use the default agent
|
// In production, it would use the default agent
|
||||||
if err != nil {
|
if err != nil {
|
||||||
assert.Contains(t, err.Error(), "agent call failed")
|
assert.Contains(t, err.Error(), "call failed")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -314,6 +314,10 @@ func TestInputFormatterClockContext(t *testing.T) {
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// createTestRobot creates a test robot with specified inspiration agent
|
// createTestRobot creates a test robot with specified inspiration agent
|
||||||
|
// Includes available expert agents so the Inspiration Agent knows what resources are available
|
||||||
|
//
|
||||||
|
// Note: The agent IDs listed in Resources.Agents must exist in yao-dev-app/assistants/experts/
|
||||||
|
// Current available experts: data-analyst, summarizer, text-writer, web-reader
|
||||||
func createTestRobot(t *testing.T, agentID string) *types.Robot {
|
func createTestRobot(t *testing.T, agentID string) *types.Robot {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
return &types.Robot{
|
return &types.Robot{
|
||||||
|
|
@ -323,12 +327,24 @@ func createTestRobot(t *testing.T, agentID string) *types.Robot {
|
||||||
Config: &types.Config{
|
Config: &types.Config{
|
||||||
Identity: &types.Identity{
|
Identity: &types.Identity{
|
||||||
Role: "Test Assistant",
|
Role: "Test Assistant",
|
||||||
Duties: []string{"Testing"},
|
Duties: []string{"Testing", "Data Analysis", "Report Generation"},
|
||||||
},
|
},
|
||||||
Resources: &types.Resources{
|
Resources: &types.Resources{
|
||||||
Phases: map[types.Phase]string{
|
Phases: map[types.Phase]string{
|
||||||
types.PhaseInspiration: agentID,
|
types.PhaseInspiration: agentID,
|
||||||
},
|
},
|
||||||
|
// Available expert agents that can be delegated to
|
||||||
|
// These IDs correspond to assistants in yao-dev-app/assistants/experts/
|
||||||
|
Agents: []string{
|
||||||
|
"experts.data-analyst", // Data analysis and insights
|
||||||
|
"experts.summarizer", // Content summarization
|
||||||
|
"experts.text-writer", // Report and document generation
|
||||||
|
"experts.web-reader", // Web content extraction
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Knowledge base collections (if any)
|
||||||
|
KB: &types.KB{
|
||||||
|
Collections: []string{"test-knowledge"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue