feat: enable capability-based routing in HandoffTool
The LLM can now delegate tasks by capability instead of requiring a specific agent_id. The handoff tool resolves the first matching agent via FindAgentsByCapability. - HandoffTool.Execute: accept "capability" as alternative to "agent_id" - HandoffTool.Description: display agent capabilities in tool listing - HandoffTool.Parameters: "task" is the only required field now - 4 new tests (route by capability, not found, no target, description)
This commit is contained in:
parent
e121c7ca04
commit
d8255f1660
2 changed files with 91 additions and 13 deletions
|
|
@ -192,6 +192,73 @@ func TestHandoffTool_Description(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestHandoffTool_Description_WithCapabilities(t *testing.T) {
|
||||
resolver := newMockResolver(
|
||||
&AgentInfo{ID: "main", Name: "Main"},
|
||||
&AgentInfo{ID: "coder", Name: "Coder", Role: "coding", Capabilities: []string{"coding", "review"}},
|
||||
)
|
||||
bb := NewBlackboard()
|
||||
tool := NewHandoffTool(resolver, bb, "main")
|
||||
|
||||
desc := tool.Description()
|
||||
if !strings.Contains(desc, "coding, review") {
|
||||
t.Errorf("Description = %q, expected capabilities", desc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandoffTool_ExecuteByCapability(t *testing.T) {
|
||||
provider := &mockProvider{response: "capability result"}
|
||||
resolver := newMockResolver(
|
||||
&AgentInfo{ID: "main", Name: "Main", Provider: provider, Tools: tools.NewToolRegistry(), MaxIter: 5},
|
||||
&AgentInfo{ID: "coder", Name: "Coder", Capabilities: []string{"coding"}, Provider: provider, Tools: tools.NewToolRegistry(), MaxIter: 5},
|
||||
)
|
||||
bb := NewBlackboard()
|
||||
tool := NewHandoffTool(resolver, bb, "main")
|
||||
|
||||
result := tool.Execute(context.Background(), map[string]any{
|
||||
"capability": "coding",
|
||||
"task": "write a function",
|
||||
})
|
||||
|
||||
if result.IsError {
|
||||
t.Fatalf("handoff by capability failed: %s", result.ForLLM)
|
||||
}
|
||||
if !strings.Contains(result.ForLLM, "capability result") {
|
||||
t.Errorf("ForLLM = %q, expected 'capability result'", result.ForLLM)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandoffTool_ExecuteByCapability_NotFound(t *testing.T) {
|
||||
resolver := newMockResolver(
|
||||
&AgentInfo{ID: "main", Name: "Main"},
|
||||
)
|
||||
bb := NewBlackboard()
|
||||
tool := NewHandoffTool(resolver, bb, "main")
|
||||
|
||||
result := tool.Execute(context.Background(), map[string]any{
|
||||
"capability": "nonexistent",
|
||||
"task": "do something",
|
||||
})
|
||||
|
||||
if !result.IsError {
|
||||
t.Error("expected error for unknown capability")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandoffTool_ExecuteNoAgentNoCapability(t *testing.T) {
|
||||
resolver := newMockResolver()
|
||||
bb := NewBlackboard()
|
||||
tool := NewHandoffTool(resolver, bb, "main")
|
||||
|
||||
result := tool.Execute(context.Background(), map[string]any{
|
||||
"task": "do something",
|
||||
})
|
||||
|
||||
if !result.IsError {
|
||||
t.Error("expected error when neither agent_id nor capability provided")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListAgentsTool_Execute(t *testing.T) {
|
||||
resolver := newMockResolver(
|
||||
&AgentInfo{ID: "main", Name: "Main Agent", Role: "general"},
|
||||
|
|
|
|||
|
|
@ -51,6 +51,9 @@ func (t *HandoffTool) Description() string {
|
|||
if a.Role != "" {
|
||||
fmt.Fprintf(&sb, ": %s", a.Role)
|
||||
}
|
||||
if len(a.Capabilities) > 0 {
|
||||
fmt.Fprintf(&sb, " [%s]", strings.Join(a.Capabilities, ", "))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
return sb.String()
|
||||
|
|
@ -63,7 +66,11 @@ func (t *HandoffTool) Parameters() map[string]any {
|
|||
"properties": map[string]any{
|
||||
"agent_id": map[string]any{
|
||||
"type": "string",
|
||||
"description": "The ID of the target agent to hand off to",
|
||||
"description": "The ID of the target agent to hand off to (required if capability is not set)",
|
||||
},
|
||||
"capability": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Route to an agent with this capability instead of by ID (e.g. \"coding\", \"research\")",
|
||||
},
|
||||
"task": map[string]any{
|
||||
"type": "string",
|
||||
|
|
@ -74,7 +81,7 @@ func (t *HandoffTool) Parameters() map[string]any {
|
|||
"description": "Optional key-value context to share via blackboard before handoff",
|
||||
},
|
||||
},
|
||||
"required": []string{"agent_id", "task"},
|
||||
"required": []string{"task"},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -86,22 +93,26 @@ func (t *HandoffTool) SetContext(channel, chatID string) {
|
|||
|
||||
// Execute delegates a task to the specified target agent.
|
||||
func (t *HandoffTool) Execute(ctx context.Context, args map[string]any) *tools.ToolResult {
|
||||
agentID, ok := args["agent_id"].(string)
|
||||
if !ok {
|
||||
agentID = ""
|
||||
}
|
||||
task, ok := args["task"].(string)
|
||||
if !ok {
|
||||
task = ""
|
||||
}
|
||||
agentID, _ := args["agent_id"].(string)
|
||||
capability, _ := args["capability"].(string)
|
||||
task, _ := args["task"].(string)
|
||||
|
||||
if agentID == "" {
|
||||
return tools.ErrorResult("agent_id is required")
|
||||
}
|
||||
if task == "" {
|
||||
return tools.ErrorResult("task is required")
|
||||
}
|
||||
|
||||
// Resolve agent: by ID or by capability
|
||||
if agentID == "" && capability != "" {
|
||||
matches := FindAgentsByCapability(t.resolver, capability)
|
||||
if len(matches) == 0 {
|
||||
return tools.ErrorResult(fmt.Sprintf("no agent found with capability %q", capability))
|
||||
}
|
||||
agentID = matches[0].ID
|
||||
}
|
||||
if agentID == "" {
|
||||
return tools.ErrorResult("agent_id or capability is required")
|
||||
}
|
||||
|
||||
// Parse optional context map
|
||||
var contextMap map[string]string
|
||||
if ctxRaw, ok := args["context"].(map[string]any); ok {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue