From 56e6fcf1a411a6e48fcc3aaf0eca0a6e17f812bb Mon Sep 17 00:00:00 2001 From: Mery-Sanz Date: Thu, 24 Apr 2025 15:03:13 +0200 Subject: [PATCH] fix agent command --- examples/cai/.gitkeep | 0 src/cai/cli.py | 5 +- src/cai/repl/commands/agent.py | 201 +++++++++++++++------------------ src/cai/util.py | 91 +++++++-------- 4 files changed, 130 insertions(+), 167 deletions(-) delete mode 100644 examples/cai/.gitkeep diff --git a/examples/cai/.gitkeep b/examples/cai/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/cai/cli.py b/src/cai/cli.py index 53c9ce05..bdbb54e7 100644 --- a/src/cai/cli.py +++ b/src/cai/cli.py @@ -280,8 +280,9 @@ def run_cai_cli(starting_agent, context_variables=None, stream=False, max_turns= if commands_handle_command(command, args): continue # Command was handled, continue to next iteration - # If command wasn't recognized, show error - console.print(f"[red]Unknown command: {command}[/red]") + # If command wasn't recognized, show error (skip for /shell or /s) + if command not in ("/shell", "/s"): + console.print(f"[red]Unknown command: {command}[/red]") continue # Process the conversation with the agent diff --git a/src/cai/repl/commands/agent.py b/src/cai/repl/commands/agent.py index 046d4d3e..b23194a3 100644 --- a/src/cai/repl/commands/agent.py +++ b/src/cai/repl/commands/agent.py @@ -155,50 +155,45 @@ class AgentCommand(Command): table = Table(title="Available Agents") table.add_column("#", style="dim") table.add_column("Name", style="cyan") - table.add_column("Module", style="magenta") + table.add_column("Key", style="magenta") + table.add_column("Module", style="green") table.add_column("Description", style="green") - table.add_column("Pattern", style="blue") - table.add_column("Model", style="yellow") - # Scan all agents from the agents folder + # Retrieve all registered agents agents_to_display = get_available_agents() - # Display all agents - for i, (name, agent) in enumerate(agents_to_display.items(), 1): - description = agent.description - if not description and hasattr(agent, 'instructions'): - if callable(agent.instructions): - description = agent.instructions(context_variables={}) - else: - description = agent.instructions - # Clean up description - remove newlines and strip spaces + 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] + "..." - # Get the module name for the agent - module_name = get_agent_module(name) + # Module where this agent lives + module_name = get_agent_module(agent_key) - # Get the pattern if it exists - pattern = getattr(agent, 'pattern', '') - if pattern: - pattern = pattern.capitalize() - - # Handle model display based on agent type - model_display = self._get_model_display(name, agent) + # Add a row with all collected info table.add_row( - str(i), - name, + str(idx), + display_name, + agent_key, module_name, - description, - pattern, - model_display + description ) console.print(table) return True + 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. @@ -210,76 +205,41 @@ class AgentCommand(Command): """ if not args: console.print("[red]Error: No agent specified[/red]") - console.print("Usage: /agent select ") + console.print("Usage: /agent select ") return False agent_id = args[0] - # Get the list of available agents agents_to_display = get_available_agents() + agent_list = list(agents_to_display.items()) # preserve order for indexing # Check if agent_id is a number if agent_id.isdigit(): index = int(agent_id) - if 1 <= index <= len(agents_to_display): - agent_name = list(agents_to_display.keys())[index - 1] + 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]") + console.print(f"[red]Error: Invalid agent number: {agent_id}[/red]") return False else: - # Treat as agent name - agent_name = agent_id - if agent_name not in agents_to_display: - console.print(f"[red]Error: Unknown agent: {agent_name}[/red]") + # Treat as agent key + for key, agent_obj in agents_to_display.items(): + if key == agent_id: + agent = agent_obj + agent_name = getattr(agent_obj, "name", key) + break + else: + console.print(f"[red]Error: Unknown agent key: {agent_id}[/red]") return False - # Get the agent - agent = agents_to_display[agent_name] - - # Set the agent as the current agent in the REPL - # We need to avoid circular imports, so we'll use a different approach - # to access the client and current_agent variables - - # Import the module dynamically to avoid circular imports - if 'cai.repl.repl' in sys.modules: - repl_module = sys.modules['cai.repl.repl'] - - # Check if client is initialized - if hasattr(repl_module, 'client') and repl_module.client: - # Update the active_agent in the client - repl_module.client.active_agent = agent - - # Update the global current_agent variable if it exists - if hasattr(repl_module, 'current_agent'): - repl_module.current_agent = agent - - # Update the global agent variable if it exists - if hasattr(repl_module, 'agent'): - repl_module.agent = agent - - # Also update the agent variable in the run_demo_loop - # function's frame if possible - try: - for frame_info in inspect.stack(): - frame = frame_info.frame - if ('run_demo_loop' in frame.f_code.co_name and - 'agent' in frame.f_locals): - frame.f_locals['agent'] = agent - break - except Exception: # pylint: disable=broad-except # nosec - # If this fails, we still have the global current_agent as - # a fallback - pass - - console.print( - f"[green]Switched to agent: {agent_name}[/green]") - visualize_agent_graph(agent) - return True - console.print("[red]Error: CAI client not initialized[/red]") - return False - console.print("[red]Error: REPL module not initialized[/red]") - return False + os.environ["CAI_MODEL"] = agent_name + console.print( + f"[green]Switched to agent: {agent_name}[/green]") + visualize_agent_graph(agent) + return True def handle_info(self, args: Optional[List[str]] = None) -> bool: """Handle /agent info command. @@ -292,57 +252,74 @@ class AgentCommand(Command): """ if not args: console.print("[red]Error: No agent specified[/red]") - console.print("Usage: /agent info ") + console.print("Usage: /agent info ") return False agent_id = args[0] - # Get the list of available agents + # Get available agents agents_to_display = get_available_agents() - # Check if agent_id is a number + # Resolve agent_id to an agent key (by index or name) if agent_id.isdigit(): - index = int(agent_id) - if 1 <= index <= len(agents_to_display): - agent_name = list(agents_to_display.keys())[index - 1] - else: - console.print( - f"[red]Error: Invalid agent number: {agent_id}[/red]") + 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: - # Treat as agent name - agent_name = agent_id - if agent_name not in agents_to_display: - console.print(f"[red]Error: Unknown agent: {agent_name}[/red]") + 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 - # Get the agent - agent = agents_to_display[agent_name] + agent = agents_to_display[agent_key] - # Display agent information + # 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 [] - # Handle model display based on agent type - model_display = self._get_model_display_for_info(agent_name, agent) - - # Create a markdown table for agent details + # Build markdown content for agent info markdown_content = f""" -# Agent: {agent_name} +# Agent Info: {name} -| Property | Value | -|----------|-------| -| Name | {agent.name} | -| Model | {model_display} | -| Functions | {len(agent.functions)} | -| Parallel Tool Calls | {'Yes' if agent.parallel_tool_calls else 'No'} | +| 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 diff --git a/src/cai/util.py b/src/cai/util.py index 63cedaf5..81703c68 100644 --- a/src/cai/util.py +++ b/src/cai/util.py @@ -268,87 +268,72 @@ def load_prompt_template(template_path): except Exception as e: raise ValueError(f"Failed to load template '{template_path}': {str(e)}") +# Start of Selection def visualize_agent_graph(start_agent): """ Visualize agent graph showing all bidirectional connections between agents. Uses Rich library for pretty printing. """ - console = Console() # pylint: disable=redefined-outer-name + console = Console() if start_agent is None: console.print("[red]No agent provided to visualize.[/red]") return - tree = Tree( - f"🤖 { - start_agent.name} (Current Agent)", - guide_style="bold blue") + tree = Tree(f"🤖 {start_agent.name} (Current Agent)", guide_style="bold blue") - # Track visited agents and their nodes to handle cross-connections - visited = {} + visited = set() agent_nodes = {} - agent_positions = {} # Track positions in tree - position_counter = 0 # Counter for tracking positions + agent_positions = {} + position_counter = 0 - def add_agent_node(agent, parent=None, is_transfer=False): # pylint: disable=too-many-branches # noqa: E501 - """Add agent node and track for cross-connections""" + def add_agent_node(agent, parent=None, is_transfer=False): + """Add an agent node and track for cross-connections.""" nonlocal position_counter - if agent is None: return None + aid = id(agent) + if aid in visited: + if is_transfer and parent: + original_pos = agent_positions.get(aid) + parent.add(f"[cyan]↩ Return to {agent.name} (Agent #{original_pos})[/cyan]") + return agent_nodes.get(aid) - # Create or get existing node for this agent - if id(agent) in visited: - if is_transfer: - # Add reference with position for repeated agents - original_pos = agent_positions[id(agent)] - parent.add( - f"[cyan]↩ Return to { - agent.name} (Top Level Agent #{original_pos})[/cyan]") - return agent_nodes[id(agent)] - - visited[id(agent)] = True + visited.add(aid) position_counter += 1 - agent_positions[id(agent)] = position_counter + agent_positions[aid] = position_counter - # Create node for current agent - if is_transfer: + if is_transfer and parent: node = parent + elif parent: + node = parent.add(f"[green]{agent.name} (#{position_counter})[/green]") else: - node = parent.add( - f"[green]{agent.name} (#{position_counter})[/green]") if parent else tree # noqa: E501 pylint: disable=line-too-long - agent_nodes[id(agent)] = node + node = tree + agent_nodes[aid] = node - # Add tools as children + # Add tools tools_node = node.add("[yellow]Tools[/yellow]") - for fn in getattr(agent, "functions", []): - if callable(fn): - fn_name = getattr(fn, "__name__", "") - if ("handoff" not in fn_name.lower() and - not fn_name.startswith("transfer_to")): - tools_node.add(f"[blue]{fn_name}[/blue]") + for tool in getattr(agent, "tools", []): + tool_name = getattr(tool, "name", None) or getattr(tool, "__name__", "") + tools_node.add(f"[blue]{tool_name}[/blue]") - # Add Handoffs section + # Add handoffs transfers_node = node.add("[magenta]Handoffs[/magenta]") + for handoff_fn in getattr(agent, "handoffs", []): + if callable(handoff_fn): + try: + next_agent = handoff_fn() + if next_agent: + transfer_node = transfers_node.add(f"🤖 {next_agent.name}") + add_agent_node(next_agent, transfer_node, True) + except Exception: + continue - # Process handoff functions - for fn in getattr(agent, "functions", []): # pylint: disable=too-many-nested-blocks # noqa: E501 - if callable(fn): - fn_name = getattr(fn, "__name__", "") - if ("handoff" in fn_name.lower() or - fn_name.startswith("transfer_to")): - try: - next_agent = fn() - if next_agent: - # Show bidirectional connection - transfer = transfers_node.add( - f"🤖 {next_agent.name}") # noqa: E501 - add_agent_node(next_agent, transfer, True) - except Exception: # nosec: B112 # pylint: disable=broad-exception-caught # noqa: E501 - continue return node - # Start recursive traversal from root agent + + # Start traversal from the root agent add_agent_node(start_agent) console.print(tree) +# End of Selectio def fix_litellm_transcription_annotations(): """