diff --git a/cookbook/anthropic_agent_sdk/README.md b/cookbook/anthropic_agent_sdk/README.md index f1132618091..294d949e24e 100644 --- a/cookbook/anthropic_agent_sdk/README.md +++ b/cookbook/anthropic_agent_sdk/README.md @@ -22,10 +22,24 @@ litellm --config config.yaml ### 3. Run the chat +**Basic Agent (no MCP):** + ```bash python main.py ``` +**Agent with MCP (DeepWiki2 for research):** + +```bash +python agent_with_mcp.py +``` + +If MCP connection fails, you can disable it: + +```bash +USE_MCP=false python agent_with_mcp.py +``` + That's it! You can now chat with the agent in your terminal. ### Chat Commands @@ -45,11 +59,19 @@ Set these environment variables if needed: ```bash export LITELLM_PROXY_URL="http://localhost:4000" export LITELLM_API_KEY="sk-1234" -export LITELLM_MODEL="claude-sonnet-4-20250514" +export LITELLM_MODEL="bedrock-claude-sonnet-4.5" ``` Or just use the defaults - it'll connect to `http://localhost:4000` by default. +## Files + +- `main.py` - Basic interactive agent without MCP +- `agent_with_mcp.py` - Agent with MCP server integration (DeepWiki2) +- `common.py` - Shared utilities and functions +- `config.example.yaml` - Example LiteLLM configuration +- `requirements.txt` - Python dependencies + ## Example Config File If you want to use multiple models, create a `config.yaml` (see `config.example.yaml`): @@ -110,6 +132,11 @@ Note: Don't add `/anthropic` to the base URL - LiteLLM handles the routing autom - Check the model name matches what's in your LiteLLM config - Run `litellm --model your-model` to test it works +**Agent with MCP stuck or failing?** +- The MCP server might not be available at `http://localhost:4000/mcp/deepwiki2` +- Try disabling MCP: `USE_MCP=false python agent_with_mcp.py` +- Or use the basic agent: `python main.py` + ## Learn More - [LiteLLM Docs](https://docs.litellm.ai/) diff --git a/cookbook/anthropic_agent_sdk/agent_with_mcp.py b/cookbook/anthropic_agent_sdk/agent_with_mcp.py new file mode 100644 index 00000000000..ff25feb777f --- /dev/null +++ b/cookbook/anthropic_agent_sdk/agent_with_mcp.py @@ -0,0 +1,140 @@ +""" +Interactive Claude Agent SDK CLI with MCP Support + +This example demonstrates an interactive CLI chat with the Anthropic Agent SDK using LiteLLM as a proxy, +with MCP (Model Context Protocol) server integration for enhanced capabilities. +""" + +import asyncio +import os +from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions +from common import ( + Config, + fetch_available_models, + setup_litellm_env, + print_header, + handle_model_list, + handle_model_switch, + stream_response, +) + + +async def interactive_chat_with_mcp(): + """ + Interactive CLI chat with the agent and MCP server + """ + config = Config() + + # Configure Anthropic SDK to point to LiteLLM gateway + litellm_base_url = setup_litellm_env(config) + + # Fetch available models from proxy + available_models = await fetch_available_models(litellm_base_url, config.LITELLM_API_KEY) + + current_model = config.LITELLM_MODEL + + # MCP server configuration + mcp_server_url = f"{litellm_base_url}/mcp/deepwiki2" + use_mcp = os.getenv("USE_MCP", "true").lower() == "true" + + if not use_mcp: + print("āš ļø MCP disabled via USE_MCP=false") + + print_header(litellm_base_url, current_model, has_mcp=use_mcp) + + while True: + # Configure agent options + if use_mcp: + try: + # Try with MCP server (HTTP transport) + # Using McpHttpServerConfig format from Agent SDK + options = ClaudeAgentOptions( + system_prompt="You are a helpful AI assistant with access to DeepWiki for research. Be concise, accurate, and friendly.", + model=current_model, + max_turns=50, + mcp_servers={ + "deepwiki2": { + "type": "http", + "url": mcp_server_url, + "headers": { + "Authorization": f"Bearer {config.LITELLM_API_KEY}" + } + } + }, + ) + except Exception as e: + print(f"āš ļø Warning: Could not configure MCP server: {e}") + print("Continuing without MCP...\n") + use_mcp = False + options = ClaudeAgentOptions( + system_prompt="You are a helpful AI assistant. Be concise, accurate, and friendly.", + model=current_model, + max_turns=50, + ) + else: + # Without MCP + options = ClaudeAgentOptions( + system_prompt="You are a helpful AI assistant. Be concise, accurate, and friendly.", + model=current_model, + max_turns=50, + ) + + # Create agent client + try: + async with ClaudeSDKClient(options=options) as client: + conversation_active = True + + while conversation_active: + # Get user input + try: + user_input = input("\nšŸ‘¤ You: ").strip() + except (EOFError, KeyboardInterrupt): + print("\n\nšŸ‘‹ Goodbye!") + return + + # Handle commands + if user_input.lower() in ['quit', 'exit']: + print("\nšŸ‘‹ Goodbye!") + return + + if user_input.lower() == 'clear': + print("\nšŸ”„ Starting new conversation...\n") + conversation_active = False + continue + + if user_input.lower() == 'models': + handle_model_list(available_models, current_model) + continue + + if user_input.lower() == 'model': + new_model, should_restart = handle_model_switch(available_models, current_model) + if should_restart: + current_model = new_model + conversation_active = False + continue + + if not user_input: + continue + + # Stream response from agent + await stream_response(client, user_input) + + except Exception as e: + print(f"\nāŒ Error creating agent client: {e}") + print("This might be an MCP configuration issue. Try running without MCP:") + print(" USE_MCP=false python agent_with_mcp.py") + print("\nOr use the basic agent:") + print(" python main.py") + return + + +def main(): + """Run interactive chat with MCP""" + try: + asyncio.run(interactive_chat_with_mcp()) + except KeyboardInterrupt: + print("\n\nšŸ‘‹ Goodbye!") + + +if __name__ == "__main__": + main() diff --git a/cookbook/anthropic_agent_sdk/common.py b/cookbook/anthropic_agent_sdk/common.py new file mode 100644 index 00000000000..d9ee65cb58d --- /dev/null +++ b/cookbook/anthropic_agent_sdk/common.py @@ -0,0 +1,160 @@ +""" +Common utilities for Claude Agent SDK examples +""" + +import os +import httpx + + +class Config: + """Configuration for LiteLLM Gateway connection""" + + # LiteLLM proxy URL (default to local instance) + LITELLM_PROXY_URL = os.getenv("LITELLM_PROXY_URL", "http://localhost:4000") + + # LiteLLM API key (master key or virtual key) + LITELLM_API_KEY = os.getenv("LITELLM_API_KEY", "sk-1234") + + # Model name as configured in LiteLLM (e.g., "bedrock-claude-sonnet-4", "gpt-4", etc.) + LITELLM_MODEL = os.getenv("LITELLM_MODEL", "bedrock-claude-sonnet-4.5") + + +async def fetch_available_models(base_url: str, api_key: str) -> list[str]: + """ + Fetch available models from LiteLLM proxy /models endpoint + """ + try: + async with httpx.AsyncClient() as client: + response = await client.get( + f"{base_url}/models", + headers={"Authorization": f"Bearer {api_key}"}, + timeout=10.0 + ) + response.raise_for_status() + data = response.json() + return [model["id"] for model in data.get("data", [])] + except Exception as e: + print(f"āš ļø Warning: Could not fetch models from proxy: {e}") + print("Using default model list...") + # Fallback to default models + return [ + "bedrock-claude-sonnet-3.5", + "bedrock-claude-sonnet-4", + "bedrock-claude-sonnet-4.5", + "bedrock-claude-opus-4.5", + "bedrock-nova-premier", + ] + + +def setup_litellm_env(config: Config): + """ + Configure environment variables to point Agent SDK to LiteLLM + """ + litellm_base_url = config.LITELLM_PROXY_URL.rstrip('/') + os.environ["ANTHROPIC_BASE_URL"] = litellm_base_url + os.environ["ANTHROPIC_API_KEY"] = config.LITELLM_API_KEY + return litellm_base_url + + +def print_header(base_url: str, current_model: str, has_mcp: bool = False): + """ + Print the chat header + """ + mcp_indicator = " + MCP" if has_mcp else "" + print("=" * 70) + print(f"šŸ¤– Claude Agent SDK with LiteLLM Gateway{mcp_indicator} - Interactive Chat") + print("=" * 70) + print(f"šŸš€ Connected to: {base_url}") + print(f"šŸ“¦ Current model: {current_model}") + if has_mcp: + print("šŸ”Œ MCP: deepwiki2 enabled") + print("\nType your messages below. Commands:") + print(" - 'quit' or 'exit' to end the conversation") + print(" - 'clear' to start a new conversation") + print(" - 'model' to switch models") + print(" - 'models' to list available models") + print("=" * 70) + print() + + +def handle_model_list(available_models: list[str], current_model: str): + """ + Display available models + """ + print("\nšŸ“‹ Available models:") + for i, model in enumerate(available_models, 1): + marker = "āœ“" if model == current_model else " " + print(f" {marker} {i}. {model}") + + +def handle_model_switch(available_models: list[str], current_model: str) -> tuple[str, bool]: + """ + Handle model switching + + Returns: + tuple: (new_model, should_restart_conversation) + """ + print("\nšŸ“‹ Select a model:") + for i, model in enumerate(available_models, 1): + marker = "āœ“" if model == current_model else " " + print(f" {marker} {i}. {model}") + + try: + choice = input("\nEnter number (or press Enter to cancel): ").strip() + if choice: + idx = int(choice) - 1 + if 0 <= idx < len(available_models): + new_model = available_models[idx] + print(f"\nāœ… Switched to: {new_model}") + print("šŸ”„ Starting new conversation with new model...\n") + return new_model, True + else: + print("āŒ Invalid choice") + except (ValueError, IndexError): + print("āŒ Invalid input") + + return current_model, False + + +async def stream_response(client, user_input: str): + """ + Stream response from the agent + """ + print("\nšŸ¤– Assistant: ", end='', flush=True) + + try: + await client.query(user_input) + + # Show loading indicator + print("ā³ thinking...", end='', flush=True) + + # Stream the response + first_chunk = True + async for msg in client.receive_response(): + # Clear loading indicator on first message + if first_chunk: + print("\ršŸ¤– Assistant: ", end='', flush=True) + first_chunk = False + + # Handle different message types + if hasattr(msg, 'type'): + if msg.type == 'content_block_delta': + # Streaming text delta + if hasattr(msg, 'delta') and hasattr(msg.delta, 'text'): + print(msg.delta.text, end='', flush=True) + elif msg.type == 'content_block_start': + # Start of content block + if hasattr(msg, 'content_block') and hasattr(msg.content_block, 'text'): + print(msg.content_block.text, end='', flush=True) + + # Fallback to original content handling + if hasattr(msg, 'content'): + for content_block in msg.content: + if hasattr(content_block, 'text'): + print(content_block.text, end='', flush=True) + + print() # New line after response + + except Exception as e: + print(f"\r\nāŒ Error: {e}") + print("Please check your LiteLLM gateway is running and configured correctly.") diff --git a/cookbook/anthropic_agent_sdk/main.py b/cookbook/anthropic_agent_sdk/main.py index 9bdd2f7364c..231b57ca97b 100644 --- a/cookbook/anthropic_agent_sdk/main.py +++ b/cookbook/anthropic_agent_sdk/main.py @@ -6,50 +6,17 @@ LiteLLM acts as a unified interface, allowing you to use any LLM provider (OpenA through the Claude Agent SDK by pointing it to the LiteLLM gateway. """ -import os import asyncio -import httpx from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions - - -class Config: - """Configuration for LiteLLM Gateway connection""" - - # LiteLLM proxy URL (default to local instance) - LITELLM_PROXY_URL = os.getenv("LITELLM_PROXY_URL", "http://localhost:4000") - - # LiteLLM API key (master key or virtual key) - LITELLM_API_KEY = os.getenv("LITELLM_API_KEY", "sk-1234") - - # Model name as configured in LiteLLM (e.g., "bedrock-claude-sonnet-4", "gpt-4", etc.) - LITELLM_MODEL = os.getenv("LITELLM_MODEL", "bedrock-claude-sonnet-4.5") - - -async def fetch_available_models(base_url: str, api_key: str) -> list[str]: - """ - Fetch available models from LiteLLM proxy /models endpoint - """ - try: - async with httpx.AsyncClient() as client: - response = await client.get( - f"{base_url}/models", - headers={"Authorization": f"Bearer {api_key}"}, - timeout=10.0 - ) - response.raise_for_status() - data = response.json() - return [model["id"] for model in data.get("data", [])] - except Exception as e: - print(f"āš ļø Warning: Could not fetch models from proxy: {e}") - print("Using default model list...") - # Fallback to default models - return [ - "bedrock-claude-sonnet-3.5", - "bedrock-claude-sonnet-4", - "bedrock-claude-sonnet-4.5", - "bedrock-claude-opus-4.5", - "bedrock-nova-premier", - ] +from common import ( + Config, + fetch_available_models, + setup_litellm_env, + print_header, + handle_model_list, + handle_model_switch, + stream_response, +) async def interactive_chat(): @@ -59,28 +26,14 @@ async def interactive_chat(): config = Config() # Configure Anthropic SDK to point to LiteLLM gateway - # Note: We don't add /anthropic to the base URL - LiteLLM handles routing - litellm_base_url = config.LITELLM_PROXY_URL.rstrip('/') - os.environ["ANTHROPIC_BASE_URL"] = litellm_base_url - os.environ["ANTHROPIC_API_KEY"] = config.LITELLM_API_KEY + litellm_base_url = setup_litellm_env(config) # Fetch available models from proxy available_models = await fetch_available_models(litellm_base_url, config.LITELLM_API_KEY) current_model = config.LITELLM_MODEL - print("=" * 70) - print("šŸ¤– Claude Agent SDK with LiteLLM Gateway - Interactive Chat") - print("=" * 70) - print(f"šŸš€ Connected to: {litellm_base_url}") - print(f"šŸ“¦ Current model: {current_model}") - print("\nType your messages below. Commands:") - print(" - 'quit' or 'exit' to end the conversation") - print(" - 'clear' to start a new conversation") - print(" - 'model' to switch models") - print(" - 'models' to list available models") - print("=" * 70) - print() + print_header(litellm_base_url, current_model) while True: # Configure agent options for each conversation @@ -113,75 +66,21 @@ async def interactive_chat(): continue if user_input.lower() == 'models': - print("\nšŸ“‹ Available models:") - for i, model in enumerate(available_models, 1): - marker = "āœ“" if model == current_model else " " - print(f" {marker} {i}. {model}") + handle_model_list(available_models, current_model) continue if user_input.lower() == 'model': - print("\nšŸ“‹ Select a model:") - for i, model in enumerate(available_models, 1): - marker = "āœ“" if model == current_model else " " - print(f" {marker} {i}. {model}") - - try: - choice = input("\nEnter number (or press Enter to cancel): ").strip() - if choice: - idx = int(choice) - 1 - if 0 <= idx < len(available_models): - current_model = available_models[idx] - print(f"\nāœ… Switched to: {current_model}") - print("šŸ”„ Starting new conversation with new model...\n") - conversation_active = False - else: - print("āŒ Invalid choice") - except (ValueError, IndexError): - print("āŒ Invalid input") + new_model, should_restart = handle_model_switch(available_models, current_model) + if should_restart: + current_model = new_model + conversation_active = False continue if not user_input: continue - # Send query to agent with loading indicator - print("\nšŸ¤– Assistant: ", end='', flush=True) - - try: - await client.query(user_input) - - # Show loading indicator - print("ā³ thinking...", end='', flush=True) - - # Stream the response - first_chunk = True - async for msg in client.receive_response(): - # Clear loading indicator on first message - if first_chunk: - print("\ršŸ¤– Assistant: ", end='', flush=True) - first_chunk = False - - # Handle different message types - if hasattr(msg, 'type'): - if msg.type == 'content_block_delta': - # Streaming text delta - if hasattr(msg, 'delta') and hasattr(msg.delta, 'text'): - print(msg.delta.text, end='', flush=True) - elif msg.type == 'content_block_start': - # Start of content block - if hasattr(msg, 'content_block') and hasattr(msg.content_block, 'text'): - print(msg.content_block.text, end='', flush=True) - - # Fallback to original content handling - if hasattr(msg, 'content'): - for content_block in msg.content: - if hasattr(content_block, 'text'): - print(content_block.text, end='', flush=True) - - print() # New line after response - - except Exception as e: - print(f"\r\nāŒ Error: {e}") - print("Please check your LiteLLM gateway is running and configured correctly.") + # Stream response from agent + await stream_response(client, user_input) def main():