From 120cf7ac994b0c65218d7498f3724c5972915a55 Mon Sep 17 00:00:00 2001 From: CrazyBoyM Date: Sat, 24 Jan 2026 23:31:48 +0800 Subject: [PATCH] refactor: remove multi-provider support, use Anthropic SDK directly - Remove provider_utils.py (241 lines of adapter code) - Simplify all agent files to use Anthropic SDK directly - Update model to claude-sonnet-4-5-20250929 - Add python-dotenv with override=True (.env takes priority over env vars) - Simplify .env.example to only require ANTHROPIC_API_KEY This keeps the codebase focused on teaching agent concepts rather than API compatibility layers. Users who need other providers can use tools like litellm or one-api. Co-Authored-By: Claude Opus 4.5 --- .env.example | 24 +---- README.md | 6 +- README_zh.md | 5 +- provider_utils.py | 242 ------------------------------------------ requirements.txt | 3 - v0_bash_agent.py | 11 +- v0_bash_agent_mini.py | 4 +- v1_basic_agent.py | 14 +-- v2_todo_agent.py | 12 +-- v3_subagent.py | 12 +-- v4_skills_agent.py | 12 +-- 11 files changed, 34 insertions(+), 311 deletions(-) delete mode 100644 provider_utils.py diff --git a/.env.example b/.env.example index ca1f096..ea70bdb 100644 --- a/.env.example +++ b/.env.example @@ -1,21 +1,3 @@ -# Provider Selection (defaults to anthropic for backward compatibility) -AI_PROVIDER=anthropic # Options: anthropic, openai, gemini, or any OpenAI-compatible service - -# Model Name (auto-defaults based on provider, but can be overridden) -MODEL_NAME=kimi-k2-turbo-preview - -# Anthropic Configuration -ANTHROPIC_API_KEY=sk-xxx -ANTHROPIC_BASE_URL=https://api.moonshot.cn/anthropic - -# OpenAI Configuration -OPENAI_API_KEY=sk-xxx -OPENAI_BASE_URL=https://api.openai.com/v1 - -# Google Gemini Configuration (via OpenAI-compatible endpoint) -GEMINI_API_KEY=xxx -GEMINI_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai/ - -# Example: Custom OpenAI-compatible service -# CUSTOM_API_KEY=xxx -# CUSTOM_BASE_URL=https://api.custom-service.com/v1 +# Anthropic API Key (required) +# Get your key at: https://console.anthropic.com/ +ANTHROPIC_API_KEY=sk-ant-xxx diff --git a/README.md b/README.md index 7c619e6..73f367f 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,11 @@ A progressive tutorial that demystifies AI coding agents like Kode, Claude Code, ```bash # Install dependencies -pip install -r requirements.txt +pip install anthropic python-dotenv -# Configure your API +# Configure API key cp .env.example .env -# Edit .env with your API key (supports Anthropic, OpenAI, Gemini, etc.) +# Edit .env with your ANTHROPIC_API_KEY # Run any version python v0_bash_agent.py # Minimal diff --git a/README_zh.md b/README_zh.md index 92d2c01..ca69b80 100644 --- a/README_zh.md +++ b/README_zh.md @@ -37,11 +37,12 @@ ## 快速开始 ```bash +# 安装依赖 pip install anthropic python-dotenv -# 配置 API +# 配置 API Key cp .env.example .env -# 编辑 .env 填入你的 API key +# 编辑 .env 填入你的 ANTHROPIC_API_KEY # 运行任意版本 python v0_bash_agent.py # 极简版 diff --git a/provider_utils.py b/provider_utils.py deleted file mode 100644 index 668c7b0..0000000 --- a/provider_utils.py +++ /dev/null @@ -1,242 +0,0 @@ -""" -Provider utilities for multi-provider AI agent support. - -This module provides a unified interface for multiple AI providers (Anthropic, OpenAI, Gemini), -allowing the existing agent code (v0-v4) to run unchanged. - -It uses the Adapter Pattern to make OpenAI-compatible clients look exactly like -Anthropic clients to the consuming code. -""" - -import os -import json -from typing import Any, Dict, List, Union, Optional -from dotenv import load_dotenv - -# Load environment variables -load_dotenv() - -# ============================================================================= -# Data Structures (Mimic Anthropic SDK) -# ============================================================================= - -class ResponseWrapper: - """Wrapper to make OpenAI responses look like Anthropic responses.""" - def __init__(self, content, stop_reason): - self.content = content - self.stop_reason = stop_reason - -class ContentBlock: - """Wrapper to make content blocks look like Anthropic content blocks.""" - def __init__(self, block_type, **kwargs): - self.type = block_type - for key, value in kwargs.items(): - setattr(self, key, value) - - def __repr__(self): - attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items()) - return f"ContentBlock({attrs})" - -# ============================================================================= -# Adapters -# ============================================================================= - -class OpenAIAdapter: - """ - Adapts the OpenAI client to look like an Anthropic client. - - Key Magic: - self.messages = self - - This allows the agent code to call: - client.messages.create(...) - - which resolves to: - adapter.create(...) - """ - def __init__(self, openai_client): - self.client = openai_client - self.messages = self # Duck typing: act as the 'messages' resource - - def create(self, model: str, system: str, messages: List[Dict], tools: List[Dict], max_tokens: int = 8000): - """ - The core translation layer. - Converts Anthropic inputs -> OpenAI inputs -> OpenAI API -> Anthropic outputs. - """ - # 1. Convert Messages (Anthropic -> OpenAI) - openai_messages = [{"role": "system", "content": system}] - - for msg in messages: - role = msg["role"] - content = msg["content"] - - if role == "user": - if isinstance(content, str): - # Simple text message - openai_messages.append({"role": "user", "content": content}) - elif isinstance(content, list): - # Tool results (User role in Anthropic, Tool role in OpenAI) - for part in content: - if part.get("type") == "tool_result": - openai_messages.append({ - "role": "tool", - "tool_call_id": part["tool_use_id"], - "content": part["content"] or "(no output)" - }) - # Note: Anthropic user messages can also contain text+image, - # but v0-v4 agents don't use that yet. - - elif role == "assistant": - if isinstance(content, str): - # Simple text message - openai_messages.append({"role": "assistant", "content": content}) - elif isinstance(content, list): - # Tool calls (Assistant role) - # Anthropic splits thought (text) and tool_use into blocks - # OpenAI puts thought in 'content' and tools in 'tool_calls' - text_parts = [] - tool_calls = [] - - for part in content: - # Handle both dicts and objects (ContentBlock) - if isinstance(part, dict): - part_type = part.get("type") - part_text = part.get("text") - part_id = part.get("id") - part_name = part.get("name") - part_input = part.get("input") - else: - part_type = getattr(part, "type", None) - part_text = getattr(part, "text", None) - part_id = getattr(part, "id", None) - part_name = getattr(part, "name", None) - part_input = getattr(part, "input", None) - - if part_type == "text": - text_parts.append(part_text) - elif part_type == "tool_use": - tool_calls.append({ - "id": part_id, - "type": "function", - "function": { - "name": part_name, - "arguments": json.dumps(part_input) - } - }) - - assistant_msg = {"role": "assistant"} - if text_parts: - assistant_msg["content"] = "\n".join(text_parts) - if tool_calls: - assistant_msg["tool_calls"] = tool_calls - - openai_messages.append(assistant_msg) - - # 2. Convert Tools (Anthropic -> OpenAI) - openai_tools = [] - for tool in tools: - openai_tools.append({ - "type": "function", - "function": { - "name": tool["name"], - "description": tool["description"], - "parameters": tool["input_schema"] - } - }) - - # 3. Call OpenAI API - # Note: Gemini/OpenAI handle max_tokens differently, but usually support the param - response = self.client.chat.completions.create( - model=model, - messages=openai_messages, - tools=openai_tools if openai_tools else None, - max_tokens=max_tokens - ) - - # 4. Convert Response (OpenAI -> Anthropic) - message = response.choices[0].message - content_blocks = [] - - # Extract text content - if message.content: - content_blocks.append(ContentBlock("text", text=message.content)) - - # Extract tool calls - if message.tool_calls: - for tool_call in message.tool_calls: - content_blocks.append(ContentBlock( - "tool_use", - id=tool_call.id, - name=tool_call.function.name, - input=json.loads(tool_call.function.arguments) - )) - - # Map stop reasons: OpenAI "stop"/"tool_calls" -> Anthropic "end_turn"/"tool_use" - # OpenAI: stop, length, content_filter, tool_calls - finish_reason = response.choices[0].finish_reason - if finish_reason == "tool_calls": - stop_reason = "tool_use" - elif finish_reason == "stop": - stop_reason = "end_turn" - else: - stop_reason = finish_reason # Fallback - - return ResponseWrapper(content_blocks, stop_reason) - -# ============================================================================= -# Factory Functions -# ============================================================================= - -def get_provider(): - """Get the current AI provider from environment variable.""" - return os.getenv("AI_PROVIDER", "anthropic").lower() - -def get_client(): - """ - Return a client that conforms to the Anthropic interface. - - If AI_PROVIDER is 'anthropic', returns the native Anthropic client. - Otherwise, returns an OpenAIAdapter wrapping an OpenAI-compatible client. - """ - provider = get_provider() - - if provider == "anthropic": - from anthropic import Anthropic - base_url = os.getenv("ANTHROPIC_BASE_URL") - # Return native client - guarantees 100% behavior compatibility - return Anthropic( - api_key=os.getenv("ANTHROPIC_API_KEY"), - base_url=base_url - ) - - else: - # For OpenAI/Gemini, we wrap the client to mimic Anthropic - try: - from openai import OpenAI - except ImportError: - raise ImportError("Please install openai: pip install openai") - - if provider == "openai": - api_key = os.getenv("OPENAI_API_KEY") - base_url = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1") - elif provider == "gemini": - api_key = os.getenv("GEMINI_API_KEY") - # Gemini OpenAI-compatible endpoint - base_url = os.getenv("GEMINI_BASE_URL", "https://generativelanguage.googleapis.com/v1beta/openai/") - else: - # Generic OpenAI-compatible provider - api_key = os.getenv(f"{provider.upper()}_API_KEY") - base_url = os.getenv(f"{provider.upper()}_BASE_URL") - - if not api_key: - raise ValueError(f"API Key for {provider} is missing. Please check your .env file.") - - raw_client = OpenAI(api_key=api_key, base_url=base_url) - return OpenAIAdapter(raw_client) - -def get_model(): - """Return model name from environment variable.""" - model = os.getenv("MODEL_NAME") - if not model: - raise ValueError("MODEL_NAME environment variable is missing. Please set it in your .env file.") - return model \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 029bc76..55f896d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,2 @@ anthropic>=0.25.0 -openai>=1.0.0 python-dotenv>=1.0.0 -pygame==2.5.2 -numpy==1.24.3 \ No newline at end of file diff --git a/v0_bash_agent.py b/v0_bash_agent.py index 11902c5..3108ff8 100644 --- a/v0_bash_agent.py +++ b/v0_bash_agent.py @@ -47,14 +47,17 @@ Usage: python v0_bash_agent.py "explore src/ and summarize" """ -from provider_utils import get_client, get_model +from anthropic import Anthropic +from dotenv import load_dotenv import subprocess import sys import os -# Initialize API client and model using provider utilities -client = get_client() -MODEL = get_model() +load_dotenv(override=True) + +# Initialize Anthropic client (uses ANTHROPIC_API_KEY env var) +client = Anthropic() +MODEL = "claude-sonnet-4-5-20250929" # The ONE tool that does everything # Notice how the description teaches the model common patterns AND how to spawn subagents diff --git a/v0_bash_agent_mini.py b/v0_bash_agent_mini.py index a69a6c3..b9b0166 100644 --- a/v0_bash_agent_mini.py +++ b/v0_bash_agent_mini.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """v0_bash_agent_mini.py - Mini Claude Code (Compact)""" -from provider_utils import get_client, get_model; import subprocess as sp, sys, os -C = get_client(); M = get_model() +from anthropic import Anthropic; from dotenv import load_dotenv; import subprocess as sp, sys, os +load_dotenv(override=True); C = Anthropic(); M = "claude-sonnet-4-5-20250929" T = [{"name":"bash","description":"Shell cmd. Read:cat/grep/find/rg/ls. Write:echo>/sed. Subagent(for complex subtask): python v0_bash_agent_mini.py 'task'","input_schema":{"type":"object","properties":{"command":{"type":"string"}},"required":["command"]}}] S = f"CLI agent at {os.getcwd()}. Use bash to solve problems. Spawn subagent for complex subtasks: python v0_bash_agent_mini.py 'task'. Subagent isolates context and returns summary. Be concise." diff --git a/v1_basic_agent.py b/v1_basic_agent.py index 5e21868..7bb2a33 100644 --- a/v1_basic_agent.py +++ b/v1_basic_agent.py @@ -51,16 +51,10 @@ import subprocess import sys from pathlib import Path +from anthropic import Anthropic from dotenv import load_dotenv -# Load configuration from .env file -load_dotenv() - -# Import unified client provider -try: - from provider_utils import get_client, get_model -except ImportError: - sys.exit("Error: provider_utils.py not found. Please ensure you are in the project root.") +load_dotenv(override=True) # ============================================================================= @@ -68,8 +62,8 @@ except ImportError: # ============================================================================= WORKDIR = Path.cwd() -MODEL = get_model() -client = get_client() +MODEL = "claude-sonnet-4-5-20250929" +client = Anthropic() # ============================================================================= diff --git a/v2_todo_agent.py b/v2_todo_agent.py index 42608fa..5da8f8f 100644 --- a/v2_todo_agent.py +++ b/v2_todo_agent.py @@ -61,14 +61,10 @@ import subprocess import sys from pathlib import Path +from anthropic import Anthropic from dotenv import load_dotenv -load_dotenv() - -try: - from provider_utils import get_client, get_model -except ImportError: - sys.exit("Error: provider_utils.py not found. Please ensure you are in the project root.") +load_dotenv(override=True) # ============================================================================= @@ -77,8 +73,8 @@ except ImportError: WORKDIR = Path.cwd() -client = get_client() -MODEL = get_model() +client = Anthropic() +MODEL = "claude-sonnet-4-5-20250929" # ============================================================================= diff --git a/v3_subagent.py b/v3_subagent.py index 1731c1d..ca3f229 100644 --- a/v3_subagent.py +++ b/v3_subagent.py @@ -79,14 +79,10 @@ import sys import time from pathlib import Path +from anthropic import Anthropic from dotenv import load_dotenv -load_dotenv() - -try: - from provider_utils import get_client, get_model -except ImportError: - sys.exit("Error: provider_utils.py not found. Please ensure you are in the project root.") +load_dotenv(override=True) # ============================================================================= @@ -95,8 +91,8 @@ except ImportError: WORKDIR = Path.cwd() -client = get_client() -MODEL = get_model() +client = Anthropic() +MODEL = "claude-sonnet-4-5-20250929" # ============================================================================= diff --git a/v4_skills_agent.py b/v4_skills_agent.py index 3814f97..35c5e37 100644 --- a/v4_skills_agent.py +++ b/v4_skills_agent.py @@ -84,14 +84,10 @@ import sys import time from pathlib import Path +from anthropic import Anthropic from dotenv import load_dotenv -load_dotenv() - -try: - from provider_utils import get_client, get_model -except ImportError: - sys.exit("Error: provider_utils.py not found. Please ensure you are in the project root.") +load_dotenv(override=True) # ============================================================================= @@ -101,8 +97,8 @@ except ImportError: WORKDIR = Path.cwd() SKILLS_DIR = WORKDIR / "skills" -client = get_client() -MODEL = get_model() +client = Anthropic() +MODEL = "claude-sonnet-4-5-20250929" # =============================================================================