diff --git a/libs/deepagents-cli/deepagents_cli/README.md b/libs/deepagents-cli/deepagents_cli/README.md index 63caf24d..84e7be71 100644 --- a/libs/deepagents-cli/deepagents_cli/README.md +++ b/libs/deepagents-cli/deepagents_cli/README.md @@ -25,10 +25,10 @@ cli/ ### `main.py` - Entry Point & Main Loop - **Purpose**: CLI entry point, argument parsing, main interactive loop - **Key Functions**: - - `cli_main()` - Console script entry point (called when you run `deepagents`) - - `main()` - Async main function that orchestrates agent creation and CLI - - `simple_cli()` - Main interactive loop handling user input - - `parse_args()` - Command-line argument parsing + - `cli_main()` - Console script entry point with provider switching loop (called when you run `deepagents`) + - `main()` - Async main function that orchestrates agent creation and CLI (returns dict for special actions) + - `simple_cli()` - Main interactive loop handling user input (returns dict for provider switching) + - `parse_args()` - Command-line argument parsing (dual parser for prompts vs commands) - `check_cli_dependencies()` - Validates required packages are installed ### `config.py` - Configuration & Constants @@ -36,10 +36,13 @@ cli/ - **Key Exports**: - `COLORS` - Color scheme for terminal output - `DEEP_AGENTS_ASCII` - ASCII art banner - - `COMMANDS` - Available slash commands + - `COMMANDS` - Available slash commands (including `/provider`) - `console` - Rich Console instance - - `create_model()` - Creates OpenAI or Anthropic model based on API keys + - `SessionState` - Holds mutable session state including `preferred_provider` + - `create_model(force_provider)` - Creates OpenAI or Anthropic model with optional provider override - `get_default_coding_instructions()` - Loads default agent prompt + - `load_agent_config(agent_name)` - Load agent configuration from config.json + - `save_agent_config(agent_name, config_data)` - Save agent configuration to config.json ### `tools.py` - Custom Agent Tools - **Purpose**: Additional tools for the agent beyond built-in filesystem operations @@ -72,9 +75,9 @@ cli/ - External editor support (Ctrl+E) ### `commands.py` - Command Handlers -- **Purpose**: Handle slash commands (`/help`, `/clear`, etc.) and bash execution +- **Purpose**: Handle slash commands (`/help`, `/clear`, `/provider`, etc.) and bash execution - **Key Functions**: - - `handle_command()` - Route and execute slash commands + - `handle_command()` - Route and execute slash commands (returns dict for provider switching) - `execute_bash_command()` - Execute bash commands prefixed with `!` ### `execution.py` - Task Execution & Streaming @@ -132,12 +135,20 @@ Type `@filename` and press Tab to autocomplete and inject file content into your ### Interactive Commands - `/help` - Show help - `/clear` - Clear screen and reset conversation +- `/provider [openai|anthropic]` - Switch between OpenAI and Anthropic providers - `/tokens` - Show token usage - `/quit` or `/exit` - Exit the CLI ### Bash Commands Type `!command` to execute bash commands directly (e.g., `!ls`, `!git status`) +### Provider Switching +Switch between OpenAI and Anthropic providers during your session: +- Use `/provider openai` to switch to OpenAI (gpt-5-mini) +- Use `/provider anthropic` to switch to Anthropic (claude-sonnet-4-5-20250929) +- The agent will be recreated with the new provider, clearing the conversation history +- Your provider choice is persisted in `~/.deepagents/AGENT_NAME/config.json` and will be used in future sessions + ### Todo List Tracking The agent can create and update a visual todo list for multi-step tasks. @@ -162,6 +173,7 @@ Each agent stores its state in `~/.deepagents/AGENT_NAME/`: - `agent.md` - Agent's custom instructions (long-term memory) - `memories/` - Additional context files - `history` - Command history +- `config.json` - Agent configuration (preferred model provider, etc.) ## Development diff --git a/libs/deepagents-cli/deepagents_cli/commands.py b/libs/deepagents-cli/deepagents_cli/commands.py index 5f2dbc1d..0cc622c9 100644 --- a/libs/deepagents-cli/deepagents_cli/commands.py +++ b/libs/deepagents-cli/deepagents_cli/commands.py @@ -9,9 +9,10 @@ from .ui import TokenTracker, show_interactive_help -def handle_command(command: str, agent, token_tracker: TokenTracker) -> str | bool: - """Handle slash commands. Returns 'exit' to exit, True if handled, False to pass to agent.""" - cmd = command.lower().strip().lstrip("/") +def handle_command(command: str, agent, token_tracker: TokenTracker) -> str | bool | dict: + """Handle slash commands. Returns 'exit' to exit, True if handled, dict for special actions.""" + cmd_parts = command.lower().strip().lstrip("/").split() + cmd = cmd_parts[0] if cmd_parts else "" if cmd in ["quit", "exit", "q"]: return "exit" @@ -41,14 +42,31 @@ def handle_command(command: str, agent, token_tracker: TokenTracker) -> str | bo token_tracker.display_session() return True + if cmd == "provider": + if len(cmd_parts) < 2: + console.print() + console.print("[yellow]Usage: /provider [openai|anthropic][/yellow]") + console.print("[dim]Switch between OpenAI and Anthropic providers.[/dim]") + console.print() + return True + + provider = cmd_parts[1].lower() + if provider not in ["openai", "anthropic"]: + console.print() + console.print(f"[yellow]Invalid provider: {provider}[/yellow]") + console.print("[dim]Valid options: openai, anthropic[/dim]") + console.print() + return True + + # Signal to recreate agent with new provider + return {"action": "recreate", "provider": provider} + console.print() console.print(f"[yellow]Unknown command: /{cmd}[/yellow]") console.print("[dim]Type /help for available commands.[/dim]") console.print() return True - return False - def execute_bash_command(command: str) -> bool: """Execute a bash command and display output. Returns True if handled.""" diff --git a/libs/deepagents-cli/deepagents_cli/config.py b/libs/deepagents-cli/deepagents_cli/config.py index f3065247..ee36e297 100644 --- a/libs/deepagents-cli/deepagents_cli/config.py +++ b/libs/deepagents-cli/deepagents_cli/config.py @@ -5,6 +5,8 @@ from pathlib import Path import dotenv +from langchain_anthropic import ChatAnthropic +from langchain_openai import ChatOpenAI from rich.console import Console dotenv.load_dotenv() @@ -40,6 +42,7 @@ COMMANDS = { "clear": "Clear screen and reset conversation", "help": "Show help information", + "provider": "Switch between OpenAI and Anthropic providers", "tokens": "Show token usage for current session", "quit": "Exit the CLI", "exit": "Exit the CLI", @@ -59,8 +62,9 @@ class SessionState: """Holds mutable session state (auto-approve mode, etc).""" - def __init__(self, auto_approve: bool = False): + def __init__(self, auto_approve: bool = False, preferred_provider: str | None = None): self.auto_approve = auto_approve + self.preferred_provider = preferred_provider def toggle_auto_approve(self) -> bool: """Toggle auto-approve and return new state.""" @@ -78,9 +82,53 @@ def get_default_coding_instructions() -> str: return default_prompt_path.read_text() -def create_model(): +def _create_openai_model(): + """Create an OpenAI model instance.""" + console.print("[dim]Using OpenAI model: gpt-5-mini[/dim]") + return ChatOpenAI( + model="gpt-5-mini", + temperature=0.7, + ) + + +def _create_anthropic_model(): + """Create an Anthropic model instance.""" + console.print("[dim]Using Anthropic model: claude-sonnet-4-5-20250929[/dim]") + return ChatAnthropic( + model_name="claude-sonnet-4-5-20250929", + max_tokens=20000, + ) + + +def _show_api_key_error(provider: str | None = None): + """Show error message for missing API key and exit.""" + if provider == "openai": + console.print("[bold red]Error:[/bold red] OPENAI_API_KEY not configured.") + console.print("\nPlease set your OpenAI API key:") + console.print(" export OPENAI_API_KEY=your_api_key_here") + elif provider == "anthropic": + console.print("[bold red]Error:[/bold red] ANTHROPIC_API_KEY not configured.") + console.print("\nPlease set your Anthropic API key:") + console.print(" export ANTHROPIC_API_KEY=your_api_key_here") + else: + console.print("[bold red]Error:[/bold red] No API key configured.") + console.print("\nPlease set one of the following environment variables:") + console.print(" - OPENAI_API_KEY (for OpenAI models like gpt-5-mini)") + console.print(" - ANTHROPIC_API_KEY (for Claude models)") + console.print("\nExample:") + console.print(" export OPENAI_API_KEY=your_api_key_here") + + console.print("\nOr add it to your .env file.") + sys.exit(1) + + +def create_model(force_provider: str | None = None): """Create the appropriate model based on available API keys. + Args: + force_provider: Optional provider to force ("openai" or "anthropic"). + If specified, only that provider will be used. + Returns: ChatModel instance (OpenAI or Anthropic) @@ -90,29 +138,72 @@ def create_model(): openai_key = os.environ.get("OPENAI_API_KEY") anthropic_key = os.environ.get("ANTHROPIC_API_KEY") + # Determine which provider to use + if force_provider == "openai": + if not openai_key: + _show_api_key_error("openai") + return _create_openai_model() + + if force_provider == "anthropic": + if not anthropic_key: + _show_api_key_error("anthropic") + return _create_anthropic_model() + + # Default behavior: prefer OpenAI, fallback to Anthropic if openai_key: - from langchain_openai import ChatOpenAI - - model_name = os.environ.get("OPENAI_MODEL", "gpt-5-mini") - console.print(f"[dim]Using OpenAI model: {model_name}[/dim]") - return ChatOpenAI( - model=model_name, - temperature=0.7, - ) + return _create_openai_model() if anthropic_key: - from langchain_anthropic import ChatAnthropic - - model_name = os.environ.get("ANTHROPIC_MODEL", "claude-sonnet-4-5-20250929") - console.print(f"[dim]Using Anthropic model: {model_name}[/dim]") - return ChatAnthropic( - model_name=model_name, - max_tokens=20000, - ) - console.print("[bold red]Error:[/bold red] No API key configured.") - console.print("\nPlease set one of the following environment variables:") - console.print(" - OPENAI_API_KEY (for OpenAI models like gpt-5-mini)") - console.print(" - ANTHROPIC_API_KEY (for Claude models)") - console.print("\nExample:") - console.print(" export OPENAI_API_KEY=your_api_key_here") - console.print("\nOr add it to your .env file.") - sys.exit(1) + return _create_anthropic_model() + + _show_api_key_error() + + +def load_agent_config(agent_name: str) -> dict: + """Load agent configuration from config.json. + + Args: + agent_name: Name of the agent + + Returns: + Dictionary with config data, empty dict if file doesn't exist + """ + import json + from pathlib import Path + + config_path = Path.home() / ".deepagents" / agent_name / "config.json" + + if not config_path.exists(): + return {} + + try: + with open(config_path) as f: + return json.load(f) + except (json.JSONDecodeError, IOError): + # If file is corrupted or unreadable, return empty config + return {} + + +def save_agent_config(agent_name: str, config_data: dict) -> None: + """Save agent configuration to config.json. + + Args: + agent_name: Name of the agent + config_data: Dictionary to save as JSON + """ + import json + from datetime import datetime, timezone + from pathlib import Path + + agent_dir = Path.home() / ".deepagents" / agent_name + agent_dir.mkdir(parents=True, exist_ok=True) + + config_path = agent_dir / "config.json" + + # Add timestamp + config_data["last_updated"] = datetime.now(timezone.utc).isoformat() + + try: + with open(config_path, "w") as f: + json.dump(config_data, f, indent=2) + except IOError as e: + console.print(f"[yellow]Warning: Could not save config: {e}[/yellow]") diff --git a/libs/deepagents-cli/deepagents_cli/main.py b/libs/deepagents-cli/deepagents_cli/main.py index c8d221b2..11dd2e5c 100644 --- a/libs/deepagents-cli/deepagents_cli/main.py +++ b/libs/deepagents-cli/deepagents_cli/main.py @@ -7,7 +7,15 @@ from .agent import create_agent_with_config, list_agents, reset_agent from .commands import execute_bash_command, handle_command -from .config import COLORS, DEEP_AGENTS_ASCII, SessionState, console, create_model +from .config import ( + COLORS, + DEEP_AGENTS_ASCII, + SessionState, + console, + create_model, + load_agent_config, + save_agent_config, +) from .execution import execute_task from .input import create_prompt_session from .tools import http_request, tavily_client, web_search @@ -93,8 +101,10 @@ def parse_args(): return parser.parse_args() -async def simple_cli(agent, assistant_id: str | None, session_state, baseline_tokens: int = 0): - """Main CLI loop.""" +async def simple_cli( + agent, assistant_id: str | None, session_state, baseline_tokens: int = 0 +) -> dict | None: + """Main CLI loop. Returns dict for special actions like provider switching.""" console.clear() console.print(DEEP_AGENTS_ASCII, style=f"bold {COLORS['primary']}") console.print() @@ -153,6 +163,9 @@ async def simple_cli(agent, assistant_id: str | None, session_state, baseline_to if result == "exit": console.print("\nGoodbye!", style=COLORS["primary"]) break + if isinstance(result, dict): + # Special action like provider switching + return result if result: # Command was handled, continue to next input continue @@ -170,10 +183,10 @@ async def simple_cli(agent, assistant_id: str | None, session_state, baseline_to await execute_task(user_input, agent, assistant_id, session_state, token_tracker) -async def main(assistant_id: str, session_state): - """Main entry point.""" +async def main(assistant_id: str, session_state) -> dict | None: + """Main entry point. Returns dict for special actions like provider switching.""" # Create the model (checks API keys) - model = create_model() + model = create_model(session_state.preferred_provider) # Create agent with conditional tools tools = [http_request] @@ -191,9 +204,10 @@ async def main(assistant_id: str, session_state): baseline_tokens = calculate_baseline_tokens(model, agent_dir, system_prompt) try: - await simple_cli(agent, assistant_id, session_state, baseline_tokens) + return await simple_cli(agent, assistant_id, session_state, baseline_tokens) except Exception as e: console.print(f"\n[bold red]❌ Error:[/bold red] {e}\n") + return None def cli_main(): @@ -211,11 +225,40 @@ def cli_main(): elif args.command == "reset": reset_agent(args.agent, args.source_agent) else: - # Create session state from args - session_state = SessionState(auto_approve=args.auto_approve) + # Load agent config to get preferred provider + agent_config = load_agent_config(args.agent) + + # Create session state from args and config + session_state = SessionState( + auto_approve=args.auto_approve, + preferred_provider=agent_config.get("preferred_provider"), + ) # API key validation happens in create_model() - asyncio.run(main(args.agent, session_state)) + # Loop to handle provider switching + while True: + result = asyncio.run(main(args.agent, session_state)) + + # Check if we need to recreate with different provider + if isinstance(result, dict) and result.get("action") == "recreate": + provider = result.get("provider") + console.print() + console.print( + f"[{COLORS['primary']}]Switching to {provider.upper()}...[/{COLORS['primary']}]" + ) + console.print() + + # Update session state + session_state.preferred_provider = provider + + # Save to config for next session + save_agent_config(args.agent, {"preferred_provider": provider}) + + # Continue loop to recreate agent + continue + + # Normal exit + break except KeyboardInterrupt: # Clean exit on Ctrl+C - suppress ugly traceback console.print("\n\n[yellow]Interrupted[/yellow]")