mirror of https://github.com/aliasrobotics/cai.git
330 lines
11 KiB
Python
330 lines
11 KiB
Python
"""
|
|
Agent "command" for CAI CLI abstraction
|
|
|
|
Provides commands for managing and switching between agents.
|
|
"""
|
|
|
|
# Standard library imports
|
|
import inspect
|
|
import os
|
|
import sys
|
|
|
|
from typing import List, Optional
|
|
|
|
# Third-party imports
|
|
from rich.console import Console # pylint: disable=import-error
|
|
from rich.markdown import Markdown # pylint: disable=import-error
|
|
from rich.table import Table # pylint: disable=import-error
|
|
|
|
# Local imports
|
|
from cai.agents import get_available_agents, get_agent_module
|
|
from cai.repl.commands.base import Command, register_command
|
|
from cai.sdk.agents import Agent
|
|
from cai.util import visualize_agent_graph
|
|
|
|
console = Console()
|
|
|
|
|
|
class AgentCommand(Command):
|
|
"""Command for managing and switching between agents."""
|
|
|
|
def __init__(self):
|
|
"""Initialize the agent command."""
|
|
# Initialize with basic parameters
|
|
super().__init__(
|
|
name="/agent",
|
|
description="Manage and switch between agents",
|
|
aliases=["/a"]
|
|
)
|
|
|
|
# Add subcommands manually
|
|
self._subcommands = {
|
|
"list": "List available agents",
|
|
"select": "Select an agent by name or number",
|
|
"info": "Show information about an agent",
|
|
"multi": "Enable multi-agent mode"
|
|
}
|
|
|
|
def _get_model_display(self, agent_name: str, agent: Agent) -> str:
|
|
"""Get the display string for an agent's model.
|
|
|
|
Args:
|
|
agent_name: Name of the agent
|
|
agent: Agent instance
|
|
|
|
Returns:
|
|
String to display for the agent's model
|
|
"""
|
|
# For code agent, always show the model
|
|
if agent_name == "code":
|
|
return agent.model
|
|
|
|
# For other agents, check if CTF_MODEL is set
|
|
ctf_model = os.getenv('CTF_MODEL')
|
|
if ctf_model and agent.model == ctf_model:
|
|
# Don't show default model for CTF_MODEL in table
|
|
# but show "Default CTF Model" in info
|
|
return ""
|
|
|
|
# Show the model from environment variable if available
|
|
env_var_name = f"CAI_{agent_name.upper()}_MODEL"
|
|
model_env = os.getenv(env_var_name)
|
|
if model_env:
|
|
return model_env
|
|
|
|
return agent.model
|
|
|
|
def _get_model_display_for_info(
|
|
self, agent_name: str, agent: Agent) -> str:
|
|
"""Get the display string for an agent's model in the info view.
|
|
|
|
Args:
|
|
agent_name: Name of the agent
|
|
agent: Agent instance
|
|
|
|
Returns:
|
|
String to display for the agent's model in the info view
|
|
"""
|
|
# For code agent, always show the model
|
|
if agent_name == "code":
|
|
return agent.model
|
|
|
|
# For other agents, check if CTF_MODEL is set
|
|
ctf_model = os.getenv('CTF_MODEL')
|
|
if ctf_model and agent.model == ctf_model:
|
|
# Show "Default CTF Model" in info
|
|
return "Default CTF Model"
|
|
|
|
# Show the model from environment variable if available
|
|
env_var_name = f"CAI_{agent_name.upper()}_MODEL"
|
|
model_env = os.getenv(env_var_name)
|
|
if model_env:
|
|
return model_env
|
|
|
|
return agent.model
|
|
|
|
def get_subcommands(self) -> List[str]:
|
|
"""Get list of subcommand names.
|
|
|
|
Returns:
|
|
List of subcommand names
|
|
"""
|
|
return list(self._subcommands.keys())
|
|
|
|
def get_subcommand_description(self, subcommand: str) -> str:
|
|
"""Get description for a subcommand.
|
|
|
|
Args:
|
|
subcommand: Name of the subcommand
|
|
|
|
Returns:
|
|
Description of the subcommand
|
|
"""
|
|
return self._subcommands.get(subcommand, "")
|
|
|
|
def handle(self, args: Optional[List[str]] = None) -> bool:
|
|
"""Handle the agent command.
|
|
|
|
Args:
|
|
args: Optional list of command arguments
|
|
|
|
Returns:
|
|
True if the command was handled successfully, False otherwise
|
|
"""
|
|
if not args:
|
|
return self.handle_list(args)
|
|
|
|
subcommand = args[0]
|
|
if subcommand in self._subcommands:
|
|
handler = getattr(self, f"handle_{subcommand}", None)
|
|
if handler:
|
|
return handler(args[1:] if len(args) > 1 else None)
|
|
|
|
# If not a subcommand, try to select an agent by name
|
|
return self.handle_select(args)
|
|
|
|
def handle_list(self, args: Optional[List[str]] = None) -> bool: # pylint: disable=unused-argument # noqa: E501
|
|
"""Handle /agent list command.
|
|
|
|
Args:
|
|
args: Optional list of command arguments (not used)
|
|
|
|
Returns:
|
|
True if the command was handled successfully
|
|
"""
|
|
table = Table(title="Available Agents")
|
|
table.add_column("#", style="dim")
|
|
table.add_column("Name", style="cyan")
|
|
table.add_column("Key", style="magenta")
|
|
table.add_column("Module", style="green")
|
|
table.add_column("Description", style="green")
|
|
|
|
# Retrieve all registered agents
|
|
agents_to_display = get_available_agents()
|
|
|
|
for idx, (agent_key, agent) in enumerate(agents_to_display.items(), start=1):
|
|
# Human-friendly name (falls back to the dict key)
|
|
display_name = getattr(agent, "name", agent_key)
|
|
|
|
# Use provided description, otherwise derive from instructions
|
|
description = getattr(agent, "description", "") or ""
|
|
if not description and hasattr(agent, "instructions"):
|
|
instr = agent.instructions
|
|
description = instr(context_variables={}) if callable(instr) else instr
|
|
if isinstance(description, str):
|
|
description = " ".join(description.split())
|
|
if len(description) > 50:
|
|
description = description[:47] + "..."
|
|
|
|
# Module where this agent lives
|
|
module_name = get_agent_module(agent_key)
|
|
|
|
# Add a row with all collected info
|
|
table.add_row(
|
|
str(idx),
|
|
display_name,
|
|
agent_key,
|
|
module_name,
|
|
description
|
|
)
|
|
|
|
console.print(table)
|
|
return True
|
|
|
|
def handle_select(self, args: Optional[List[str]] = None) -> bool: # pylint: disable=too-many-branches,line-too-long # noqa: E501
|
|
"""Handle /agent select command.
|
|
|
|
Args:
|
|
args: Optional list of command arguments
|
|
|
|
Returns:
|
|
True if the command was handled successfully, False otherwise
|
|
"""
|
|
if not args:
|
|
console.print("[red]Error: No agent specified[/red]")
|
|
console.print("Usage: /agent select <agent_key|number>")
|
|
return False
|
|
|
|
agent_id = args[0]
|
|
|
|
agents_to_display = get_available_agents()
|
|
agent_list = list(agents_to_display.items())
|
|
|
|
# Check if agent_id is a number
|
|
if agent_id.isdigit():
|
|
index = int(agent_id)
|
|
if 1 <= index <= len(agent_list):
|
|
# Get the agent tuple from the list
|
|
selected_agent_key, selected_agent = agent_list[index - 1]
|
|
agent_name = getattr(selected_agent, "name", selected_agent_key)
|
|
agent = selected_agent
|
|
else:
|
|
console.print(f"[red]Error: Invalid agent number: {agent_id}[/red]")
|
|
return False
|
|
else:
|
|
# Treat as agent key
|
|
selected_agent_key = None
|
|
for key, agent_obj in agents_to_display.items():
|
|
if key == agent_id:
|
|
agent = agent_obj
|
|
selected_agent_key = key
|
|
agent_name = getattr(agent_obj, "name", key)
|
|
break
|
|
else:
|
|
console.print(f"[red]Error: Unknown agent key: {agent_id}[/red]")
|
|
return False
|
|
|
|
# Set the agent key in environment variable (not the agent name)
|
|
os.environ["CAI_AGENT_TYPE"] = selected_agent_key
|
|
|
|
console.print(
|
|
f"[green]Switched to agent: {agent_name}[/green]", end="")
|
|
visualize_agent_graph(agent)
|
|
return True
|
|
|
|
def handle_info(self, args: Optional[List[str]] = None) -> bool:
|
|
"""Handle /agent info command.
|
|
|
|
Args:
|
|
args: Optional list of command arguments
|
|
|
|
Returns:
|
|
True if the command was handled successfully, False otherwise
|
|
"""
|
|
if not args:
|
|
console.print("[red]Error: No agent specified[/red]")
|
|
console.print("Usage: /agent info <agent_key|number>")
|
|
return False
|
|
|
|
agent_id = args[0]
|
|
|
|
# Get available agents
|
|
agents_to_display = get_available_agents()
|
|
|
|
# Resolve agent_id to an agent key (by index or name)
|
|
if agent_id.isdigit():
|
|
idx = int(agent_id)
|
|
if not (1 <= idx <= len(agents_to_display)):
|
|
console.print(f"[red]Error: Invalid agent number: {agent_id}[/red]")
|
|
return False
|
|
agent_key = list(agents_to_display.keys())[idx - 1]
|
|
else:
|
|
agent_key = None
|
|
for key, ag in agents_to_display.items():
|
|
if key == agent_id or getattr(ag, "name", "").lower() == agent_id.lower():
|
|
agent_key = key
|
|
break
|
|
if agent_key is None:
|
|
console.print(f"[red]Error: Unknown agent key: {agent_id}[/red]")
|
|
return False
|
|
|
|
agent = agents_to_display[agent_key]
|
|
|
|
# Display agent information
|
|
instructions = agent.instructions
|
|
if callable(instructions):
|
|
instructions = instructions()
|
|
# Prepare agent properties
|
|
name = agent.name or agent_key
|
|
description = getattr(agent, "description", None) or "N/A"
|
|
clean_description = " ".join(line.strip() for line in description.splitlines())
|
|
functions = getattr(agent, "functions", [])
|
|
parallel = getattr(agent, "parallel_tool_calls", False)
|
|
handoff_desc = getattr(agent, "handoff_description", None) or "N/A"
|
|
handoffs = getattr(agent, "handoffs", [])
|
|
tools = getattr(agent, "tools", [])
|
|
guardrails_in = getattr(agent, "input_guardrails", [])
|
|
guardrails_out = getattr(agent, "output_guardrails", [])
|
|
output_type = getattr(agent, "output_type", None) or "N/A"
|
|
hooks = getattr(agent, "hooks", []) or []
|
|
|
|
# Build markdown content for agent info
|
|
markdown_content = f"""
|
|
# Agent Info: {name}
|
|
|
|
| Property | Value |
|
|
|------------------------|-------------------------------|
|
|
| Key | {agent_key} |
|
|
| Name | {name} |
|
|
| Description | {clean_description} |
|
|
| Functions | {len(functions)} |
|
|
| Parallel Tool Calls | {"Yes" if parallel else "No"} |
|
|
| Handoff Description | {handoff_desc} |
|
|
| Handoffs | {len(handoffs)} |
|
|
| Tools | {len(tools)} |
|
|
| Input Guardrails | {len(guardrails_in)} |
|
|
| Output Guardrails | {len(guardrails_out)} |
|
|
| Output Type | {output_type} |
|
|
| Hooks | {len(hooks)} |
|
|
|
|
## Instructions
|
|
{instructions}
|
|
|
|
"""
|
|
console.print(Markdown(markdown_content))
|
|
return True
|
|
|
|
|
|
# Register the command
|
|
register_command(AgentCommand())
|