Repository: disler/single-file-agents Branch: main Commit: ae5826a4165c Files: 193 Total size: 1.1 MB Directory structure: gitextract_m5kdaf2m/ ├── .gitignore ├── CLAUDE.md ├── README.md ├── ai_docs/ │ ├── anthropic-new-text-editor.md │ ├── anthropic-token-efficient-tool-use.md │ ├── building-eff-agents.md │ ├── existing_anthropic_computer_use_code.md │ ├── fc_openai_agents.md │ ├── openai-function-calling.md │ ├── python_anthropic.md │ ├── python_genai.md │ └── python_openai.md ├── codebase-architectures/ │ ├── .gitignore │ ├── README.md │ ├── atomic-composable-architecture/ │ │ ├── README.md │ │ ├── atom/ │ │ │ ├── auth.py │ │ │ ├── notifications.py │ │ │ └── validation.py │ │ ├── main.py │ │ ├── molecule/ │ │ │ ├── alerting.py │ │ │ └── user_management.py │ │ └── organism/ │ │ ├── alerts_api.py │ │ └── user_api.py │ ├── layered-architecture/ │ │ ├── README.md │ │ ├── api/ │ │ │ ├── category_api.py │ │ │ └── product_api.py │ │ ├── data/ │ │ │ └── database.py │ │ ├── main.py │ │ ├── models/ │ │ │ ├── category.py │ │ │ └── product.py │ │ ├── services/ │ │ │ ├── category_service.py │ │ │ └── product_service.py │ │ └── utils/ │ │ └── logger.py │ ├── pipeline-architecture/ │ │ ├── README.md │ │ ├── data/ │ │ │ ├── .gitkeep │ │ │ └── sales_data.json │ │ ├── main.py │ │ ├── output/ │ │ │ ├── .gitkeep │ │ │ └── sales_analysis.json │ │ ├── pipeline_manager/ │ │ │ ├── data_pipeline.py │ │ │ └── pipeline_manager.py │ │ ├── shared/ │ │ │ └── utilities.py │ │ └── steps/ │ │ ├── input_stage.py │ │ ├── output_stage.py │ │ └── processing_stage.py │ └── vertical-slice-architecture/ │ ├── README.md │ ├── features/ │ │ ├── projects/ │ │ │ ├── README.md │ │ │ ├── api.py │ │ │ ├── model.py │ │ │ └── service.py │ │ ├── tasks/ │ │ │ ├── README.md │ │ │ ├── api.py │ │ │ ├── model.py │ │ │ └── service.py │ │ └── users/ │ │ ├── README.md │ │ ├── api.py │ │ ├── model.py │ │ └── service.py │ └── main.py ├── data/ │ ├── analytics.csv │ └── analytics.json ├── example-agent-codebase-arch/ │ ├── README.md │ ├── __init__.py │ ├── atomic-composable-architecture/ │ │ ├── __init__.py │ │ ├── atom/ │ │ │ ├── __init__.py │ │ │ ├── file_tools/ │ │ │ │ ├── __init__.py │ │ │ │ ├── insert_tool.py │ │ │ │ ├── read_tool.py │ │ │ │ ├── replace_tool.py │ │ │ │ ├── result_tool.py │ │ │ │ ├── undo_tool.py │ │ │ │ └── write_tool.py │ │ │ ├── logging/ │ │ │ │ ├── __init__.py │ │ │ │ ├── console.py │ │ │ │ └── display.py │ │ │ └── path_utils/ │ │ │ ├── __init__.py │ │ │ ├── directory.py │ │ │ ├── extension.py │ │ │ ├── normalize.py │ │ │ └── validation.py │ │ ├── membrane/ │ │ │ ├── __init__.py │ │ │ ├── main_file_agent.py │ │ │ └── mcp_file_agent.py │ │ ├── molecule/ │ │ │ ├── __init__.py │ │ │ ├── file_crud.py │ │ │ ├── file_reader.py │ │ │ └── file_writer.py │ │ └── organism/ │ │ ├── __init__.py │ │ └── file_agent.py │ └── vertical-slice-architecture/ │ ├── __init__.py │ ├── features/ │ │ ├── __init__.py │ │ ├── blog_agent/ │ │ │ ├── __init__.py │ │ │ ├── blog_agent.py │ │ │ ├── blog_manager.py │ │ │ ├── create_tool.py │ │ │ ├── delete_tool.py │ │ │ ├── model_tools.py │ │ │ ├── read_tool.py │ │ │ ├── search_tool.py │ │ │ ├── tool_handler.py │ │ │ └── update_tool.py │ │ ├── blog_agent_v2/ │ │ │ ├── __init__.py │ │ │ ├── blog_agent.py │ │ │ ├── blog_manager.py │ │ │ ├── create_tool.py │ │ │ ├── delete_tool.py │ │ │ ├── model_tools.py │ │ │ ├── read_tool.py │ │ │ ├── search_tool.py │ │ │ ├── tool_handler.py │ │ │ └── update_tool.py │ │ ├── file_agent/ │ │ │ ├── __init__.py │ │ │ ├── api_tools.py │ │ │ ├── create_tool.py │ │ │ ├── file_agent.py │ │ │ ├── file_editor.py │ │ │ ├── file_writer.py │ │ │ ├── insert_tool.py │ │ │ ├── model_tools.py │ │ │ ├── read_tool.py │ │ │ ├── replace_tool.py │ │ │ ├── service_tools.py │ │ │ ├── tool_handler.py │ │ │ └── write_tool.py │ │ ├── file_agent_v2/ │ │ │ ├── __init__.py │ │ │ ├── api_tools.py │ │ │ ├── create_tool.py │ │ │ ├── file_agent.py │ │ │ ├── file_editor.py │ │ │ ├── file_writer.py │ │ │ ├── insert_tool.py │ │ │ ├── model_tools.py │ │ │ ├── read_tool.py │ │ │ ├── replace_tool.py │ │ │ ├── service_tools.py │ │ │ ├── tool_handler.py │ │ │ └── write_tool.py │ │ └── file_agent_v2_gemini/ │ │ ├── __init__.py │ │ ├── api_tools.py │ │ ├── create_tool.py │ │ ├── file_agent.py │ │ ├── file_editor.py │ │ ├── file_writer.py │ │ ├── insert_tool.py │ │ ├── model_tools.py │ │ ├── read_tool.py │ │ ├── replace_tool.py │ │ ├── service_tools.py │ │ ├── tool_handler.py │ │ └── write_tool.py │ └── main.py ├── extra/ │ ├── ai_code_basic.sh │ ├── ai_code_reflect.sh │ ├── create_db.py │ ├── gist_poc.py │ └── gist_poc.sh ├── openai-agents-examples/ │ ├── 01_basic_agent.py │ ├── 02_multi_agent.py │ ├── 03_sync_agent.py │ ├── 04_agent_with_tracing.py │ ├── 05_agent_with_function_tools.py │ ├── 06_agent_with_custom_tools.py │ ├── 07_agent_with_handoffs.py │ ├── 08_agent_with_agent_as_tool.py │ ├── 09_agent_with_context_management.py │ ├── 10_agent_with_guardrails.py │ ├── 11_agent_orchestration.py │ ├── 12_anthropic_agent.py │ ├── 13_research_blog_system.py │ ├── README.md │ ├── fix_imports.py │ ├── install_dependencies.sh │ ├── summary.md │ ├── test_all_examples.sh │ └── test_imports.py ├── sfa_bash_editor_agent_anthropic_v2.py ├── sfa_bash_editor_agent_anthropic_v3.py ├── sfa_codebase_context_agent_v3.py ├── sfa_codebase_context_agent_w_ripgrep_v3.py ├── sfa_duckdb_anthropic_v2.py ├── sfa_duckdb_gemini_v1.py ├── sfa_duckdb_gemini_v2.py ├── sfa_duckdb_openai_v2.py ├── sfa_file_editor_sonny37_v1.py ├── sfa_jq_gemini_v1.py ├── sfa_meta_prompt_openai_v1.py ├── sfa_openai_agent_sdk_v1.py ├── sfa_openai_agent_sdk_v1_minimal.py ├── sfa_poc.py ├── sfa_polars_csv_agent_anthropic_v3.py ├── sfa_polars_csv_agent_openai_v2.py ├── sfa_scrapper_agent_openai_v2.py └── sfa_sqlite_openai_v2.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .aider* session_dir/ data/* !data/mock.json !data/mock.db !data/mock.sqlite !data/analytics.json !data/analytics.db !data/analytics.sqlite !data/analytics.csv specs/ patterns.log paic-patterns.log .env relevant_files.json output_relevant_files.json package-lock.json agent_workspace/ __pycache__/ *.pyc *.pyo *.pyd ================================================ FILE: CLAUDE.md ================================================ # CLAUDE.md - Single File Agents Repository ## Commands - **Run agents**: `uv run [options]` ## Environment - Set API keys before running agents: ```bash export GEMINI_API_KEY='your-api-key-here' export OPENAI_API_KEY='your-api-key-here' export ANTHROPIC_API_KEY='your-api-key-here' export FIRECRAWL_API_KEY='your-api-key-here' ``` ## Code Style - Single file agents with embedded dependencies (using `uv`) - Dependencies specified at top of file in `/// script` comments - Include example usage in docstrings - Detailed error handling with user-friendly messages - Consistent format for command-line arguments ## Structure - Each agent focuses on a single capability (DuckDB, SQLite, JQ, etc.) - Command-line arguments use argparse with consistent patterns - File naming: `sfa___v.py` ## Usage > We use astral `uv` as our python package manager. > > This enables us to run SINGLE FILE AGENTS with embedded dependencies. To run an agent, use the following command: ```bash uv run sfa___v.py ``` ================================================ FILE: README.md ================================================ # Single File Agents (SFA) > Premise: #1: What if we could pack single purpose, powerful AI Agents into a single python file? > > Premise: #2: What's the best structural pattern for building Agents that can improve in capability as compute and intelligence increases? ![Scale Your AI Coding Impact](images/scale-your-ai-coding-impact-with-devin-cursor-aider.png) ![Single File Agents](images/single-file-agents-thumb.png) ## What is this? A collection of powerful single-file agents built on top of [uv](https://github.com/astral/uv) - the modern Python package installer and resolver. These agents aim to do one thing and one thing only. They demonstrate precise prompt engineering and GenAI patterns for practical tasks many of which I share on the [IndyDevDan YouTube channel](https://www.youtube.com/@indydevdan). Watch us walk through the Single File Agent in [this video](https://youtu.be/YAIJV48QlXc). You can also check out [this video](https://youtu.be/vq-vTsbSSZ0) where we use [Devin](https://devin.ai/), [Cursor](https://www.cursor.com/), [Aider](https://aider.chat/), and [PAIC-Patterns](https://agenticengineer.com/principled-ai-coding) to build three new agents with powerful spec (plan) prompts. This repo contains a few agents built across the big 3 GenAI providers (Gemini, OpenAI, Anthropic). ## Quick Start Export your API keys: ```bash export GEMINI_API_KEY='your-api-key-here' export OPENAI_API_KEY='your-api-key-here' export ANTHROPIC_API_KEY='your-api-key-here' export FIRECRAWL_API_KEY='your-api-key-here' # Get your API key from https://www.firecrawl.dev/ ``` JQ Agent: ```bash uv run sfa_jq_gemini_v1.py --exe "Filter scores above 80 from data/analytics.json and save to high_scores.json" ``` DuckDB Agent (OpenAI): ```bash # Tip tier uv run sfa_duckdb_openai_v2.py -d ./data/analytics.db -p "Show me all users with score above 80" ``` DuckDB Agent (Anthropic): ```bash # Tip tier uv run sfa_duckdb_anthropic_v2.py -d ./data/analytics.db -p "Show me all users with score above 80" ``` DuckDB Agent (Gemini): ```bash # Buggy but usually works uv run sfa_duckdb_gemini_v2.py -d ./data/analytics.db -p "Show me all users with score above 80" ``` SQLite Agent (OpenAI): ```bash uv run sfa_sqlite_openai_v2.py -d ./data/analytics.sqlite -p "Show me all users with score above 80" ``` Meta Prompt Generator: ```bash uv run sfa_meta_prompt_openai_v1.py \ --purpose "generate mermaid diagrams" \ --instructions "generate a mermaid valid chart, use diagram type specified or default flow, use examples to understand the structure of the output" \ --sections "user-prompt" \ --variables "user-prompt" ``` ### Bash Editor Agent (Anthropic) > (sfa_bash_editor_agent_anthropic_v2.py) An AI-powered assistant that can both edit files and execute bash commands using Claude's tool use capabilities. Example usage: ```bash # View a file uv run sfa_bash_editor_agent_anthropic_v2.py --prompt "Show me the first 10 lines of README.md" # Create a new file uv run sfa_bash_editor_agent_anthropic_v2.py --prompt "Create a new file called hello.txt with 'Hello World!' in it" # Replace text in a file uv run sfa_bash_editor_agent_anthropic_v2.py --prompt "Create a new file called hello.txt with 'Hello World!' in it. Then update hello.txt to say 'Hello AI Coding World'" # Execute a bash command uv run sfa_bash_editor_agent_anthropic_v2.py --prompt "List all Python files in the current directory sorted by size" ``` ### Polars CSV Agent (OpenAI) > (sfa_polars_csv_agent_openai_v2.py) An AI-powered assistant that generates and executes Polars data transformations for CSV files using OpenAI's function calling capabilities. Example usage: ```bash # Run Polars CSV agent with default compute loops (10) uv run sfa_polars_csv_agent_openai_v2.py -i "data/analytics.csv" -p "What is the average age of the users?" # Run with custom compute loops uv run sfa_polars_csv_agent_openai_v2.py -i "data/analytics.csv" -p "What is the average age of the users?" -c 5 ``` ### Web Scraper Agent (OpenAI) > (sfa_scrapper_agent_openai_v2.py) An AI-powered web scraping and content filtering assistant that uses OpenAI's function calling capabilities and the Firecrawl API for efficient web scraping. Example usage: ```bash # Basic scraping with markdown list output uv run sfa_scrapper_agent_openai_v2.py -u "https://example.com" -p "Scrap and format each sentence as a separate line in a markdown list" -o "example.md" # Advanced scraping with specific content extraction uv run sfa_scrapper_agent_openai_v2.py \ --url https://agenticengineer.com/principled-ai-coding \ --prompt "What are the names and descriptions of each lesson?" \ --output-file-path paic-lessons.md \ -c 10 ``` ## Features - **Self-contained**: Each agent is a single file with embedded dependencies - **Minimal, Precise Agents**: Carefully crafted prompts for small agents that can do one thing really well - **Modern Python**: Built on uv for fast, reliable dependency management - **Run From The Cloud**: With uv, you can run these scripts from your server or right from a gist (see my gists commands) - **Patternful**: Building effective agents is about setting up the right prompts, tools, and process for your use case. Once you setup a great pattern, you can re-use it over and over. That's part of the magic of these SFA's. ## Test Data The project includes a test duckdb database (`data/analytics.db`), a sqlite database (`data/analytics.sqlite`), and a JSON file (`data/analytics.json`) for testing purposes. The database contains sample user data with the following characteristics: ### User Table - 30 sample users with varied attributes - Fields: id (UUID), name, age, city, score, is_active, status, created_at - Test data includes: - Names: Alice, Bob, Charlie, Diana, Eric, Fiona, Jane, John - Cities: Berlin, London, New York, Paris, Singapore, Sydney, Tokyo, Toronto - Status values: active, inactive, pending, archived - Age range: 20-65 - Score range: 3.1-96.18 - Date range: 2023-2025 Perfect for testing filtering, sorting, and aggregation operations with realistic data variations. ## Agents > Note: We're using the term 'agent' loosely for some of these SFA's. We have prompts, prompt chains, and a couple are official Agents. ### JQ Command Agent > (sfa_jq_gemini_v1.py) An AI-powered assistant that generates precise jq commands for JSON processing Example usage: ```bash # Generate and execute a jq command uv run sfa_jq_gemini_v1.py --exe "Filter scores above 80 from data/analytics.json and save to high_scores.json" # Generate command only uv run sfa_jq_gemini_v1.py "Filter scores above 80 from data/analytics.json and save to high_scores.json" ``` ### DuckDB Agents > (sfa_duckdb_openai_v2.py, sfa_duckdb_anthropic_v2.py, sfa_duckdb_gemini_v2.py, sfa_duckdb_gemini_v1.py) We have three DuckDB agents that demonstrate different approaches and capabilities across major AI providers: #### DuckDB OpenAI Agent (sfa_duckdb_openai_v2.py, sfa_duckdb_openai_v1.py) An AI-powered assistant that generates and executes DuckDB SQL queries using OpenAI's function calling capabilities. Example usage: ```bash # Run DuckDB agent with default compute loops (10) uv run sfa_duckdb_openai_v2.py -d ./data/analytics.db -p "Show me all users with score above 80" # Run with custom compute loops uv run sfa_duckdb_openai_v2.py -d ./data/analytics.db -p "Show me all users with score above 80" -c 5 ``` #### DuckDB Anthropic Agent (sfa_duckdb_anthropic_v2.py) An AI-powered assistant that generates and executes DuckDB SQL queries using Claude's tool use capabilities. Example usage: ```bash # Run DuckDB agent with default compute loops (10) uv run sfa_duckdb_anthropic_v2.py -d ./data/analytics.db -p "Show me all users with score above 80" # Run with custom compute loops uv run sfa_duckdb_anthropic_v2.py -d ./data/analytics.db -p "Show me all users with score above 80" -c 5 ``` #### DuckDB Gemini Agent (sfa_duckdb_gemini_v2.py) An AI-powered assistant that generates and executes DuckDB SQL queries using Gemini's function calling capabilities. Example usage: ```bash # Run DuckDB agent with default compute loops (10) uv run sfa_duckdb_gemini_v2.py -d ./data/analytics.db -p "Show me all users with score above 80" # Run with custom compute loops uv run sfa_duckdb_gemini_v2.py -d ./data/analytics.db -p "Show me all users with score above 80" -c 5 ``` ### Meta Prompt Generator (sfa_meta_prompt_openai_v1.py) An AI-powered assistant that generates comprehensive, structured prompts for language models. Example usage: ```bash # Generate a meta prompt using command-line arguments. # Optional arguments are marked with a ?. uv run sfa_meta_prompt_openai_v1.py \ --purpose "generate mermaid diagrams" \ --instructions "generate a mermaid valid chart, use diagram type specified or default flow, use examples to understand the structure of the output" \ --sections "examples, user-prompt" \ --examples "create examples of 3 basic mermaid charts with and blocks" \ --variables "user-prompt" # Without optional arguments, the script will enter interactive mode. uv run sfa_meta_prompt_openai_v1.py \ --purpose "generate mermaid diagrams" \ --instructions "generate a mermaid valid chart, use diagram type specified or default flow, use examples to understand the structure of the output" # Interactive Mode # Just run the script without any flags to enter interactive mode. # You'll be prompted step by step for: # - Purpose (required): The main goal of your prompt # - Instructions (required): Detailed instructions for the model # - Sections (optional): Additional sections to include # - Examples (optional): Example inputs and outputs # - Variables (optional): Placeholders for dynamic content uv run sfa_meta_prompt_openai_v1.py ``` ### Git Agent > Up for a challenge? ## Requirements - Python 3.8+ - uv package manager - GEMINI_API_KEY (for Gemini-based agents) - OPENAI_API_KEY (for OpenAI-based agents) - ANTHROPIC_API_KEY (for Anthropic-based agents) - jq command-line JSON processor (for JQ agent) - DuckDB CLI (for DuckDB agents) ### Installing Required Tools #### jq Installation macOS: ```bash brew install jq ``` Windows: - Download from [stedolan.github.io/jq/download](https://stedolan.github.io/jq/download/) - Or install with Chocolatey: `choco install jq` #### DuckDB Installation macOS: ```bash brew install duckdb ``` Windows: - Download the CLI executable from [duckdb.org/docs/installation](https://duckdb.org/docs/installation) - Add the executable location to your system PATH ## Installation 1. Install uv: ```bash curl -LsSf https://astral.sh/uv/install.sh | sh ``` 2. Clone this repository: ```bash git clone ``` 3. Set your Gemini API key (for JQ generator): ```bash export GEMINI_API_KEY='your-api-key-here' # Set your OpenAI API key (for DuckDB agents): export OPENAI_API_KEY='your-api-key-here' # Set your Anthropic API key (for DuckDB agents): export ANTHROPIC_API_KEY='your-api-key-here' ``` ## Shout Outs + Resources for you - [uv](https://github.com/astral/uv) - The engineers creating uv are built different. Thank you for fixing the python ecosystem. - [Simon Willison](https://simonwillison.net) - Simon introduced me to the fact that you can [use uv to run single file python scripts](https://simonwillison.net/2024/Aug/20/uv-unified-python-packaging/) with dependencies. Massive thanks for all your work. He runs one of the most valuable blogs for engineers in the world. - [Building Effective Agents](https://www.anthropic.com/research/building-effective-agents) - A proper breakdown of how to build useful units of value built on top of GenAI. - [Part Time Larry](https://youtu.be/zm0Vo6Di3V8?si=oBetAgc5ifhBmK03) - Larry has a great breakdown on the new Python GenAI library and delivers great hands on, actionable GenAI x Finance information. - [Aider](https://aider.chat/) - AI Coding done right. Maximum control over your AI Coding Experience. Enough said. --- - [New Gemini Python SDK](https://github.com/google-gemini/generative-ai-python) - [Anthropic Agent Chatbot Example](https://github.com/anthropics/courses/blob/master/tool_use/06_chatbot_with_multiple_tools.ipynb) - [Anthropic Customer Service Agent](https://github.com/anthropics/anthropic-cookbook/blob/main/tool_use/customer_service_agent.ipynb) ## AI Coding ## Context Priming Read README.md, CLAUDE.md, ai_docs/*, and run git ls-files to understand this codebase. ## License MIT License - feel free to use this code in your own projects. If you find value from my work: give a shout out and tag my YT channel [IndyDevDan](https://www.youtube.com/@indydevdan). ================================================ FILE: ai_docs/anthropic-new-text-editor.md ================================================ Claude can use an Anthropic-defined text editor tool to view and modify text files, helping you debug, fix, and improve your code or other text documents. This allows Claude to directly interact with your files, providing hands-on assistance rather than just suggesting changes. ## Before using the text editor tool ### Use a compatible model Anthropic's text editor tool is only available for Claude 3.5 Sonnet and Claude 3.7 Sonnet: * **Claude 3.7 Sonnet**: `text_editor_20250124` * **Claude 3.5 Sonnet**: `text_editor_20241022` Both versions provide identical capabilities - the version you use should match the model you're working with. ### Assess your use case fit Some examples of when to use the text editor tool are: * **Code debugging**: Have Claude identify and fix bugs in your code, from syntax errors to logic issues. * **Code refactoring**: Let Claude improve your code structure, readability, and performance through targeted edits. * **Documentation generation**: Ask Claude to add docstrings, comments, or README files to your codebase. * **Test creation**: Have Claude create unit tests for your code based on its understanding of the implementation. --- ## Use the text editor tool Provide the text editor tool (named `str_replace_editor`) to Claude using the Messages API: The text editor tool can be used in the following way: ### Text editor tool commands The text editor tool supports several commands for viewing and modifying files: #### view The `view` command allows Claude to examine the contents of a file. It can read the entire file or a specific range of lines. Parameters: * `command`: Must be "view" * `path`: The path to the file to view * `view_range` (optional): An array of two integers specifying the start and end line numbers to view. Line numbers are 1-indexed, and -1 for the end line means read to the end of the file. #### str\_replace The `str_replace` command allows Claude to replace a specific string in a file with a new string. This is used for making precise edits. Parameters: * `command`: Must be "str\_replace" * `path`: The path to the file to modify * `old_str`: The text to replace (must match exactly, including whitespace and indentation) * `new_str`: The new text to insert in place of the old text #### create The `create` command allows Claude to create a new file with specified content. Parameters: * `command`: Must be "create" * `path`: The path where the new file should be created * `file_text`: The content to write to the new file #### insert The `insert` command allows Claude to insert text at a specific location in a file. Parameters: * `command`: Must be "insert" * `path`: The path to the file to modify * `insert_line`: The line number after which to insert the text (0 for beginning of file) * `new_str`: The text to insert #### undo\_edit The `undo_edit` command allows Claude to revert the last edit made to a file. Parameters: * `command`: Must be "undo\_edit" * `path`: The path to the file whose last edit should be undone ### Example: Fixing a syntax error with the text editor tool This example demonstrates how Claude uses the text editor tool to fix a syntax error in a Python file. First, your application provides Claude with the text editor tool and a prompt to fix a syntax error: Claude will use the text editor tool first to view the file: Your application should then read the file and return its contents to Claude: Claude will identify the syntax error and use the `str_replace` command to fix it: Your application should then make the edit and return the result: Finally, Claude will provide a complete explanation of the fix: --- ## Implement the text editor tool The text editor tool is implemented as a schema-less tool, identified by `type: "text_editor_20250124"`. When using this tool, you don't need to provide an input schema as with other tools; the schema is built into Claude's model and can't be modified. ### Handle errors When using the text editor tool, various errors may occur. Here is guidance on how to handle them: ### Follow implementation best practices --- ## Pricing and token usage The text editor tool uses the same pricing structure as other tools used with Claude. It follows the standard input and output token pricing based on the Claude model you're using. In addition to the base tokens, the following additional input tokens are needed for the text editor tool: | Tool | Additional input tokens | | --- | --- | | `text_editor_20241022` (Claude 3.5 Sonnet) | 700 tokens | | `text_editor_20250124` (Claude 3.7 Sonnet) | 700 tokens | For more detailed information about tool pricing, see [Tool use pricing](about:/en/docs/build-with-claude/tool-use#pricing). ## Integrate the text editor tool with computer use The text editor tool can be used alongside the [computer use tool](/en/docs/agents-and-tools/computer-use) and other Anthropic-defined tools. When combining these tools, you'll need to: 1. Include the appropriate beta header (if using with computer use) 2. Match the tool version with the model you're using 3. Account for the additional token usage for all tools included in your request For more information about using the text editor tool in a computer use context, see the [Computer use](/en/docs/agents-and-tools/computer-use). ## Change log | Date | Version | Changes | | --- | --- | --- | | March 13, 2025 | `text_editor_20250124` | Introduction of standalone Text Editor Tool documentation. This version is optimized for Claude 3.7 Sonnet but has identical capabilities to the previous version. | | October 22, 2024 | `text_editor_20241022` | Initial release of the Text Editor Tool with Claude 3.5 Sonnet. Provides capabilities for viewing, creating, and editing files through the `view`, `create`, `str_replace`, `insert`, and `undo_edit` commands. | ## Next steps Here are some ideas for how to use the text editor tool in more convenient and powerful ways: * **Integrate with your development workflow**: Build the text editor tool into your development tools or IDE * **Create a code review system**: Have Claude review your code and make improvements * **Build a debugging assistant**: Create a system where Claude can help you diagnose and fix issues in your code * **Implement file format conversion**: Let Claude help you convert files from one format to another * **Automate documentation**: Set up workflows for Claude to automatically document your code As you build applications with the text editor tool, we're excited to see how you leverage Claude's capabilities to enhance your development workflow and productivity. ================================================ FILE: ai_docs/anthropic-token-efficient-tool-use.md ================================================ # Token-Efficient Tool Use The upgraded Claude 3.7 Sonnet model is capable of calling tools in a token-efficient manner. Requests save an average of 14% in output tokens, up to 70%, which also reduces latency. Exact token reduction and latency improvements depend on the overall response shape and size. To use this beta feature, simply add the beta header `token-efficient-tools-2025-02-19` to a tool use request with `claude-3-7-sonnet-20250219`. If you are using the SDK, ensure that you are using the beta SDK with `anthropic.beta.messages`. Here's an example of how to use token-efficient tools with the API: ```python # Sample code to demonstrate token-efficient tools import anthropic from anthropic.beta import messages as beta_messages client = anthropic.Anthropic() # Use the beta messages endpoint with token-efficient tools response = beta_messages.create( model="claude-3-7-sonnet-20250219", max_tokens=1000, beta_features=["token-efficient-tools-2025-02-19"], tools=[{ "name": "get_weather", "description": "Get the current weather for a location", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state" } }, "required": ["location"] } }], messages=[{ "role": "user", "content": "What's the weather in San Francisco?" }] ) ``` The above request should, on average, use fewer input and output tokens than a normal request. To confirm this, try making the same request but remove `token-efficient-tools-2025-02-19` from the beta headers list. # Text Editor Tool Claude can use an Anthropic-defined text editor tool to view and modify text files, helping you debug, fix, and improve your code or other text documents. This allows Claude to directly interact with your files, providing hands-on assistance rather than just suggesting changes. ## Before using the text editor tool ### Use a compatible model Anthropic's text editor tool is only available for Claude 3.5 Sonnet and Claude 3.7 Sonnet: * **Claude 3.7 Sonnet**: `text_editor_20250124` * **Claude 3.5 Sonnet**: `text_editor_20241022` Both versions provide identical capabilities - the version you use should match the model you're working with. ### Assess your use case fit Some examples of when to use the text editor tool are: * **Code debugging**: Have Claude identify and fix bugs in your code, from syntax errors to logic issues. * **Code refactoring**: Let Claude improve your code structure, readability, and performance through targeted edits. * **Documentation generation**: Ask Claude to add docstrings, comments, or README files to your codebase. * **Test creation**: Have Claude create unit tests for your code based on its understanding of the implementation. ## Text editor tool commands The text editor tool supports several commands for viewing and modifying files: ### view The `view` command allows Claude to examine the contents of a file. It can read the entire file or a specific range of lines. Parameters: * `command`: Must be "view" * `path`: The path to the file to view * `view_range` (optional): An array of two integers specifying the start and end line numbers to view. Line numbers are 1-indexed, and -1 for the end line means read to the end of the file. ### str_replace The `str_replace` command allows Claude to replace a specific string in a file with a new string. This is used for making precise edits. Parameters: * `command`: Must be "str_replace" * `path`: The path to the file to modify * `old_str`: The text to replace (must match exactly, including whitespace and indentation) * `new_str`: The new text to insert in place of the old text ### create The `create` command allows Claude to create a new file with specified content. Parameters: * `command`: Must be "create" * `path`: The path where the new file should be created * `file_text`: The content to write to the new file ### insert The `insert` command allows Claude to insert text at a specific location in a file. Parameters: * `command`: Must be "insert" * `path`: The path to the file to modify * `insert_line`: The line number after which to insert the text (0 for beginning of file) * `new_str`: The text to insert ### undo_edit The `undo_edit` command allows Claude to revert the last edit made to a file. Parameters: * `command`: Must be "undo_edit" * `path`: The path to the file whose last edit should be undone ## Pricing and token usage The text editor tool uses the same pricing structure as other tools used with Claude. It follows the standard input and output token pricing based on the Claude model you're using. In addition to the base tokens, the following additional input tokens are needed for the text editor tool: | Tool | Additional input tokens | | --- | --- | | `text_editor_20241022` (Claude 3.5 Sonnet) | 700 tokens | | `text_editor_20250124` (Claude 3.7 Sonnet) | 700 tokens | ## Change log | Date | Version | Changes | | --- | --- | --- | | March 13, 2025 | `text_editor_20250124` | Introduction of standalone Text Editor Tool documentation. This version is optimized for Claude 3.7 Sonnet but has identical capabilities to the previous version. | | October 22, 2024 | `text_editor_20241022` | Initial release of the Text Editor Tool with Claude 3.5 Sonnet. Provides capabilities for viewing, creating, and editing files through the `view`, `create`, `str_replace`, `insert`, and `undo_edit` commands. | ================================================ FILE: ai_docs/building-eff-agents.md ================================================ Product # Building effective agents Dec 19, 2024 Over the past year, we've worked with dozens of teams building large language model (LLM) agents across industries. Consistently, the most successful implementations weren't using complex frameworks or specialized libraries. Instead, they were building with simple, composable patterns. In this post, we share what we’ve learned from working with our customers and building agents ourselves, and give practical advice for developers on building effective agents. ## What are agents? "Agent" can be defined in several ways. Some customers define agents as fully autonomous systems that operate independently over extended periods, using various tools to accomplish complex tasks. Others use the term to describe more prescriptive implementations that follow predefined workflows. At Anthropic, we categorize all these variations as **agentic systems**, but draw an important architectural distinction between **workflows** and **agents**: - **Workflows** are systems where LLMs and tools are orchestrated through predefined code paths. - **Agents**, on the other hand, are systems where LLMs dynamically direct their own processes and tool usage, maintaining control over how they accomplish tasks. Below, we will explore both types of agentic systems in detail. In Appendix 1 (“Agents in Practice”), we describe two domains where customers have found particular value in using these kinds of systems. ## When (and when not) to use agents When building applications with LLMs, we recommend finding the simplest solution possible, and only increasing complexity when needed. This might mean not building agentic systems at all. Agentic systems often trade latency and cost for better task performance, and you should consider when this tradeoff makes sense. When more complexity is warranted, workflows offer predictability and consistency for well-defined tasks, whereas agents are the better option when flexibility and model-driven decision-making are needed at scale. For many applications, however, optimizing single LLM calls with retrieval and in-context examples is usually enough. ## When and how to use frameworks There are many frameworks that make agentic systems easier to implement, including: - [LangGraph](https://langchain-ai.github.io/langgraph/) from LangChain; - Amazon Bedrock's [AI Agent framework](https://aws.amazon.com/bedrock/agents/); - [Rivet](https://rivet.ironcladapp.com/), a drag and drop GUI LLM workflow builder; and - [Vellum](https://www.vellum.ai/), another GUI tool for building and testing complex workflows. These frameworks make it easy to get started by simplifying standard low-level tasks like calling LLMs, defining and parsing tools, and chaining calls together. However, they often create extra layers of abstraction that can obscure the underlying prompts ​​and responses, making them harder to debug. They can also make it tempting to add complexity when a simpler setup would suffice. We suggest that developers start by using LLM APIs directly: many patterns can be implemented in a few lines of code. If you do use a framework, ensure you understand the underlying code. Incorrect assumptions about what's under the hood are a common source of customer error. See our [cookbook](https://github.com/anthropics/anthropic-cookbook/tree/main/patterns/agents) for some sample implementations. ## Building blocks, workflows, and agents In this section, we’ll explore the common patterns for agentic systems we’ve seen in production. We'll start with our foundational building block—the augmented LLM—and progressively increase complexity, from simple compositional workflows to autonomous agents. ### Building block: The augmented LLM The basic building block of agentic systems is an LLM enhanced with augmentations such as retrieval, tools, and memory. Our current models can actively use these capabilities—generating their own search queries, selecting appropriate tools, and determining what information to retain. ![](https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2Fd3083d3f40bb2b6f477901cc9a240738d3dd1371-2401x1000.png&w=3840&q=75)The augmented LLM We recommend focusing on two key aspects of the implementation: tailoring these capabilities to your specific use case and ensuring they provide an easy, well-documented interface for your LLM. While there are many ways to implement these augmentations, one approach is through our recently released [Model Context Protocol](https://www.anthropic.com/news/model-context-protocol), which allows developers to integrate with a growing ecosystem of third-party tools with a simple [client implementation](https://modelcontextprotocol.io/tutorials/building-a-client#building-mcp-clients). For the remainder of this post, we'll assume each LLM call has access to these augmented capabilities. ### Workflow: Prompt chaining Prompt chaining decomposes a task into a sequence of steps, where each LLM call processes the output of the previous one. You can add programmatic checks (see "gate” in the diagram below) on any intermediate steps to ensure that the process is still on track. ![](https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2F7418719e3dab222dccb379b8879e1dc08ad34c78-2401x1000.png&w=3840&q=75)The prompt chaining workflow **When to use this workflow:** This workflow is ideal for situations where the task can be easily and cleanly decomposed into fixed subtasks. The main goal is to trade off latency for higher accuracy, by making each LLM call an easier task. **Examples where prompt chaining is useful:** - Generating Marketing copy, then translating it into a different language. - Writing an outline of a document, checking that the outline meets certain criteria, then writing the document based on the outline. ### Workflow: Routing Routing classifies an input and directs it to a specialized followup task. This workflow allows for separation of concerns, and building more specialized prompts. Without this workflow, optimizing for one kind of input can hurt performance on other inputs. ![](https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2F5c0c0e9fe4def0b584c04d37849941da55e5e71c-2401x1000.png&w=3840&q=75)The routing workflow **When to use this workflow:** Routing works well for complex tasks where there are distinct categories that are better handled separately, and where classification can be handled accurately, either by an LLM or a more traditional classification model/algorithm. **Examples where routing is useful:** - Directing different types of customer service queries (general questions, refund requests, technical support) into different downstream processes, prompts, and tools. - Routing easy/common questions to smaller models like Claude 3.5 Haiku and hard/unusual questions to more capable models like Claude 3.5 Sonnet to optimize cost and speed. ### Workflow: Parallelization LLMs can sometimes work simultaneously on a task and have their outputs aggregated programmatically. This workflow, parallelization, manifests in two key variations: - **Sectioning**: Breaking a task into independent subtasks run in parallel. - **Voting:** Running the same task multiple times to get diverse outputs. ![](https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2F406bb032ca007fd1624f261af717d70e6ca86286-2401x1000.png&w=3840&q=75)The parallelization workflow **When to use this workflow:** Parallelization is effective when the divided subtasks can be parallelized for speed, or when multiple perspectives or attempts are needed for higher confidence results. For complex tasks with multiple considerations, LLMs generally perform better when each consideration is handled by a separate LLM call, allowing focused attention on each specific aspect. **Examples where parallelization is useful:** - **Sectioning**: - Implementing guardrails where one model instance processes user queries while another screens them for inappropriate content or requests. This tends to perform better than having the same LLM call handle both guardrails and the core response. - Automating evals for evaluating LLM performance, where each LLM call evaluates a different aspect of the model’s performance on a given prompt. - **Voting**: - Reviewing a piece of code for vulnerabilities, where several different prompts review and flag the code if they find a problem. - Evaluating whether a given piece of content is inappropriate, with multiple prompts evaluating different aspects or requiring different vote thresholds to balance false positives and negatives. ### Workflow: Orchestrator-workers In the orchestrator-workers workflow, a central LLM dynamically breaks down tasks, delegates them to worker LLMs, and synthesizes their results. ![](https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2F8985fc683fae4780fb34eab1365ab78c7e51bc8e-2401x1000.png&w=3840&q=75)The orchestrator-workers workflow **When to use this workflow:** This workflow is well-suited for complex tasks where you can’t predict the subtasks needed (in coding, for example, the number of files that need to be changed and the nature of the change in each file likely depend on the task). Whereas it’s topographically similar, the key difference from parallelization is its flexibility—subtasks aren't pre-defined, but determined by the orchestrator based on the specific input. **Example where orchestrator-workers is useful:** - Coding products that make complex changes to multiple files each time. - Search tasks that involve gathering and analyzing information from multiple sources for possible relevant information. ### Workflow: Evaluator-optimizer In the evaluator-optimizer workflow, one LLM call generates a response while another provides evaluation and feedback in a loop. ![](https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2F14f51e6406ccb29e695da48b17017e899a6119c7-2401x1000.png&w=3840&q=75)The evaluator-optimizer workflow **When to use this workflow:** This workflow is particularly effective when we have clear evaluation criteria, and when iterative refinement provides measurable value. The two signs of good fit are, first, that LLM responses can be demonstrably improved when a human articulates their feedback; and second, that the LLM can provide such feedback. This is analogous to the iterative writing process a human writer might go through when producing a polished document. **Examples where evaluator-optimizer is useful:** - Literary translation where there are nuances that the translator LLM might not capture initially, but where an evaluator LLM can provide useful critiques. - Complex search tasks that require multiple rounds of searching and analysis to gather comprehensive information, where the evaluator decides whether further searches are warranted. ### Agents Agents are emerging in production as LLMs mature in key capabilities—understanding complex inputs, engaging in reasoning and planning, using tools reliably, and recovering from errors. Agents begin their work with either a command from, or interactive discussion with, the human user. Once the task is clear, agents plan and operate independently, potentially returning to the human for further information or judgement. During execution, it's crucial for the agents to gain “ground truth” from the environment at each step (such as tool call results or code execution) to assess its progress. Agents can then pause for human feedback at checkpoints or when encountering blockers. The task often terminates upon completion, but it’s also common to include stopping conditions (such as a maximum number of iterations) to maintain control. Agents can handle sophisticated tasks, but their implementation is often straightforward. They are typically just LLMs using tools based on environmental feedback in a loop. It is therefore crucial to design toolsets and their documentation clearly and thoughtfully. We expand on best practices for tool development in Appendix 2 ("Prompt Engineering your Tools"). ![](https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2F58d9f10c985c4eb5d53798dea315f7bb5ab6249e-2401x1000.png&w=3840&q=75)Autonomous agent **When to use agents:** Agents can be used for open-ended problems where it’s difficult or impossible to predict the required number of steps, and where you can’t hardcode a fixed path. The LLM will potentially operate for many turns, and you must have some level of trust in its decision-making. Agents' autonomy makes them ideal for scaling tasks in trusted environments. The autonomous nature of agents means higher costs, and the potential for compounding errors. We recommend extensive testing in sandboxed environments, along with the appropriate guardrails. **Examples where agents are useful:** The following examples are from our own implementations: - A coding Agent to resolve [SWE-bench tasks](https://www.anthropic.com/research/swe-bench-sonnet), which involve edits to many files based on a task description; - Our [“computer use” reference implementation](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo), where Claude uses a computer to accomplish tasks. ![](https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2F4b9a1f4eb63d5962a6e1746ac26bbc857cf3474f-2400x1666.png&w=3840&q=75)High-level flow of a coding agent ## Combining and customizing these patterns These building blocks aren't prescriptive. They're common patterns that developers can shape and combine to fit different use cases. The key to success, as with any LLM features, is measuring performance and iterating on implementations. To repeat: you should consider adding complexity _only_ when it demonstrably improves outcomes. ## Summary Success in the LLM space isn't about building the most sophisticated system. It's about building the _right_ system for your needs. Start with simple prompts, optimize them with comprehensive evaluation, and add multi-step agentic systems only when simpler solutions fall short. When implementing agents, we try to follow three core principles: 1. Maintain **simplicity** in your agent's design. 2. Prioritize **transparency** by explicitly showing the agent’s planning steps. 3. Carefully craft your agent-computer interface (ACI) through thorough tool **documentation and testing**. Frameworks can help you get started quickly, but don't hesitate to reduce abstraction layers and build with basic components as you move to production. By following these principles, you can create agents that are not only powerful but also reliable, maintainable, and trusted by their users. ### Acknowledgements Written by Erik Schluntz and Barry Zhang. This work draws upon our experiences building agents at Anthropic and the valuable insights shared by our customers, for which we're deeply grateful. ## Appendix 1: Agents in practice Our work with customers has revealed two particularly promising applications for AI agents that demonstrate the practical value of the patterns discussed above. Both applications illustrate how agents add the most value for tasks that require both conversation and action, have clear success criteria, enable feedback loops, and integrate meaningful human oversight. ### A. Customer support Customer support combines familiar chatbot interfaces with enhanced capabilities through tool integration. This is a natural fit for more open-ended agents because: - Support interactions naturally follow a conversation flow while requiring access to external information and actions; - Tools can be integrated to pull customer data, order history, and knowledge base articles; - Actions such as issuing refunds or updating tickets can be handled programmatically; and - Success can be clearly measured through user-defined resolutions. Several companies have demonstrated the viability of this approach through usage-based pricing models that charge only for successful resolutions, showing confidence in their agents' effectiveness. ### B. Coding agents The software development space has shown remarkable potential for LLM features, with capabilities evolving from code completion to autonomous problem-solving. Agents are particularly effective because: - Code solutions are verifiable through automated tests; - Agents can iterate on solutions using test results as feedback; - The problem space is well-defined and structured; and - Output quality can be measured objectively. In our own implementation, agents can now solve real GitHub issues in the [SWE-bench Verified](https://www.anthropic.com/research/swe-bench-sonnet) benchmark based on the pull request description alone. However, whereas automated testing helps verify functionality, human review remains crucial for ensuring solutions align with broader system requirements. ## Appendix 2: Prompt engineering your tools No matter which agentic system you're building, tools will likely be an important part of your agent. [Tools](https://www.anthropic.com/news/tool-use-ga) enable Claude to interact with external services and APIs by specifying their exact structure and definition in our API. When Claude responds, it will include a [tool use block](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#example-api-response-with-a-tool-use-content-block) in the API response if it plans to invoke a tool. Tool definitions and specifications should be given just as much prompt engineering attention as your overall prompts. In this brief appendix, we describe how to prompt engineer your tools. There are often several ways to specify the same action. For instance, you can specify a file edit by writing a diff, or by rewriting the entire file. For structured output, you can return code inside markdown or inside JSON. In software engineering, differences like these are cosmetic and can be converted losslessly from one to the other. However, some formats are much more difficult for an LLM to write than others. Writing a diff requires knowing how many lines are changing in the chunk header before the new code is written. Writing code inside JSON (compared to markdown) requires extra escaping of newlines and quotes. Our suggestions for deciding on tool formats are the following: - Give the model enough tokens to "think" before it writes itself into a corner. - Keep the format close to what the model has seen naturally occurring in text on the internet. - Make sure there's no formatting "overhead" such as having to keep an accurate count of thousands of lines of code, or string-escaping any code it writes. One rule of thumb is to think about how much effort goes into human-computer interfaces (HCI), and plan to invest just as much effort in creating good _agent_-computer interfaces (ACI). Here are some thoughts on how to do so: - Put yourself in the model's shoes. Is it obvious how to use this tool, based on the description and parameters, or would you need to think carefully about it? If so, then it’s probably also true for the model. A good tool definition often includes example usage, edge cases, input format requirements, and clear boundaries from other tools. - How can you change parameter names or descriptions to make things more obvious? Think of this as writing a great docstring for a junior developer on your team. This is especially important when using many similar tools. - Test how the model uses your tools: Run many example inputs in our [workbench](https://console.anthropic.com/workbench) to see what mistakes the model makes, and iterate. - [Poka-yoke](https://en.wikipedia.org/wiki/Poka-yoke) your tools. Change the arguments so that it is harder to make mistakes. While building our agent for [SWE-bench](https://www.anthropic.com/research/swe-bench-sonnet), we actually spent more time optimizing our tools than the overall prompt. For example, we found that the model would make mistakes with tools using relative filepaths after the agent had moved out of the root directory. To fix this, we changed the tool to always require absolute filepaths—and we found that the model used this method flawlessly. [Share on Twitter](https://twitter.com/intent/tweet?text=https://www.anthropic.com/research/building-effective-agents)[Share on LinkedIn](https://www.linkedin.com/shareArticle?mini=true&url=https://www.anthropic.com/research/building-effective-agents) ================================================ FILE: ai_docs/existing_anthropic_computer_use_code.md ================================================ ```python import os import anthropic import argparse import yaml import subprocess from datetime import datetime import uuid from typing import Dict, Any, List, Optional, Union import traceback import sys import logging from logging.handlers import RotatingFileHandler EDITOR_DIR = os.path.join(os.getcwd(), "editor_dir") SESSIONS_DIR = os.path.join(os.getcwd(), "sessions") os.makedirs(SESSIONS_DIR, exist_ok=True) # Fetch system prompts from environment variables or use defaults BASH_SYSTEM_PROMPT = os.environ.get( "BASH_SYSTEM_PROMPT", "You are a helpful assistant that can execute bash commands." ) EDITOR_SYSTEM_PROMPT = os.environ.get( "EDITOR_SYSTEM_PROMPT", "You are a helpful assistant that helps users edit text files.", ) class SessionLogger: def __init__(self, session_id: str, sessions_dir: str): self.session_id = session_id self.sessions_dir = sessions_dir self.logger = self._setup_logging() # Initialize token counters self.total_input_tokens = 0 self.total_output_tokens = 0 def _setup_logging(self) -> logging.Logger: """Configure logging for the session""" log_formatter = logging.Formatter( "%(asctime)s - %(name)s - %(levelname)s - %(prefix)s - %(message)s" ) log_file = os.path.join(self.sessions_dir, f"{self.session_id}.log") file_handler = RotatingFileHandler( log_file, maxBytes=1024 * 1024, backupCount=5 ) file_handler.setFormatter(log_formatter) console_handler = logging.StreamHandler() console_handler.setFormatter(log_formatter) logger = logging.getLogger(self.session_id) logger.addHandler(file_handler) logger.addHandler(console_handler) logger.setLevel(logging.DEBUG) return logger def update_token_usage(self, input_tokens: int, output_tokens: int): """Update the total token usage.""" self.total_input_tokens += input_tokens self.total_output_tokens += output_tokens def log_total_cost(self): """Calculate and log the total cost based on token usage.""" cost_per_million_input_tokens = 3.0 # $3.00 per million input tokens cost_per_million_output_tokens = 15.0 # $15.00 per million output tokens total_input_cost = ( self.total_input_tokens / 1_000_000 ) * cost_per_million_input_tokens total_output_cost = ( self.total_output_tokens / 1_000_000 ) * cost_per_million_output_tokens total_cost = total_input_cost + total_output_cost prefix = "📊 session" self.logger.info( f"Total input tokens: {self.total_input_tokens}", extra={"prefix": prefix} ) self.logger.info( f"Total output tokens: {self.total_output_tokens}", extra={"prefix": prefix} ) self.logger.info( f"Total input cost: ${total_input_cost:.6f}", extra={"prefix": prefix} ) self.logger.info( f"Total output cost: ${total_output_cost:.6f}", extra={"prefix": prefix} ) self.logger.info(f"Total cost: ${total_cost:.6f}", extra={"prefix": prefix}) class EditorSession: def __init__(self, session_id: Optional[str] = None): """Initialize editor session with optional existing session ID""" self.session_id = session_id or self._create_session_id() self.sessions_dir = SESSIONS_DIR self.editor_dir = EDITOR_DIR self.client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY")) self.messages = [] # Create editor directory if needed os.makedirs(self.editor_dir, exist_ok=True) # Initialize logger placeholder self.logger = None # Set log prefix self.log_prefix = "📝 file_editor" def set_logger(self, session_logger: SessionLogger): """Set the logger for the session and store the SessionLogger instance.""" self.session_logger = session_logger self.logger = logging.LoggerAdapter( self.session_logger.logger, {"prefix": self.log_prefix} ) def _create_session_id(self) -> str: """Create a new session ID""" timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") return f"{timestamp}-{uuid.uuid4().hex[:6]}" def _get_editor_path(self, path: str) -> str: """Convert API path to local editor directory path""" # Strip any leading /repo/ from the path clean_path = path.replace("/repo/", "", 1) # Join with editor_dir full_path = os.path.join(self.editor_dir, clean_path) # Create the directory structure if it doesn't exist os.makedirs(os.path.dirname(full_path), exist_ok=True) return full_path def _handle_view(self, path: str, _: Dict[str, Any]) -> Dict[str, Any]: """Handle view command""" editor_path = self._get_editor_path(path) if os.path.exists(editor_path): with open(editor_path, "r") as f: return {"content": f.read()} return {"error": f"File {editor_path} does not exist"} def _handle_create(self, path: str, tool_call: Dict[str, Any]) -> Dict[str, Any]: """Handle create command""" os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w") as f: f.write(tool_call["file_text"]) return {"content": f"File created at {path}"} def _handle_str_replace( self, path: str, tool_call: Dict[str, Any] ) -> Dict[str, Any]: """Handle str_replace command""" with open(path, "r") as f: content = f.read() if tool_call["old_str"] not in content: return {"error": "old_str not found in file"} new_content = content.replace( tool_call["old_str"], tool_call.get("new_str", "") ) with open(path, "w") as f: f.write(new_content) return {"content": "File updated successfully"} def _handle_insert(self, path: str, tool_call: Dict[str, Any]) -> Dict[str, Any]: """Handle insert command""" with open(path, "r") as f: lines = f.readlines() insert_line = tool_call["insert_line"] if insert_line > len(lines): return {"error": "insert_line beyond file length"} lines.insert(insert_line, tool_call["new_str"] + "\n") with open(path, "w") as f: f.writelines(lines) return {"content": "Content inserted successfully"} def log_to_session(self, data: Dict[str, Any], section: str) -> None: """Log data to session log file""" self.logger.info(f"{section}: {data}") def handle_text_editor_tool(self, tool_call: Dict[str, Any]) -> Dict[str, Any]: """Handle text editor tool calls""" try: command = tool_call["command"] if not all(key in tool_call for key in ["command", "path"]): return {"error": "Missing required fields"} # Get path and ensure directory exists path = self._get_editor_path(tool_call["path"]) handlers = { "view": self._handle_view, "create": self._handle_create, "str_replace": self._handle_str_replace, "insert": self._handle_insert, } handler = handlers.get(command) if not handler: return {"error": f"Unknown command {command}"} return handler(path, tool_call) except Exception as e: self.logger.error(f"Error in handle_text_editor_tool: {str(e)}") return {"error": str(e)} def process_tool_calls( self, tool_calls: List[anthropic.types.ContentBlock] ) -> List[Dict[str, Any]]: """Process tool calls and return results""" results = [] for tool_call in tool_calls: if tool_call.type == "tool_use" and tool_call.name == "str_replace_editor": # Log the keys and first 20 characters of the values of the tool_call for key, value in tool_call.input.items(): truncated_value = str(value)[:20] + ( "..." if len(str(value)) > 20 else "" ) self.logger.info( f"Tool call key: {key}, Value (truncated): {truncated_value}" ) result = self.handle_text_editor_tool(tool_call.input) # Convert result to match expected tool result format is_error = False if result.get("error"): is_error = True tool_result_content = [{"type": "text", "text": result["error"]}] else: tool_result_content = [ {"type": "text", "text": result.get("content", "")} ] results.append( { "tool_call_id": tool_call.id, "output": { "type": "tool_result", "content": tool_result_content, "tool_use_id": tool_call.id, "is_error": is_error, }, } ) return results def process_edit(self, edit_prompt: str) -> None: """Main method to process editing prompts""" try: # Initial message with proper content structure api_message = { "role": "user", "content": [{"type": "text", "text": edit_prompt}], } self.messages = [api_message] self.logger.info(f"User input: {api_message}") while True: response = self.client.beta.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=4096, messages=self.messages, tools=[ {"type": "text_editor_20241022", "name": "str_replace_editor"} ], system=EDITOR_SYSTEM_PROMPT, betas=["computer-use-2024-10-22"], ) # Extract token usage from the response input_tokens = getattr(response.usage, "input_tokens", 0) output_tokens = getattr(response.usage, "output_tokens", 0) self.logger.info( f"API usage: input_tokens={input_tokens}, output_tokens={output_tokens}" ) # Update token counts in SessionLogger self.session_logger.update_token_usage(input_tokens, output_tokens) self.logger.info(f"API response: {response.model_dump()}") # Convert response content to message params response_content = [] for block in response.content: if block.type == "text": response_content.append({"type": "text", "text": block.text}) else: response_content.append(block.model_dump()) # Add assistant response to messages self.messages.append({"role": "assistant", "content": response_content}) if response.stop_reason != "tool_use": print(response.content[0].text) break tool_results = self.process_tool_calls(response.content) # Add tool results as user message if tool_results: self.messages.append( {"role": "user", "content": [tool_results[0]["output"]]} ) if tool_results[0]["output"]["is_error"]: self.logger.error( f"Error: {tool_results[0]['output']['content']}" ) break # After the execution loop, log the total cost self.session_logger.log_total_cost() except Exception as e: self.logger.error(f"Error in process_edit: {str(e)}") self.logger.error(traceback.format_exc()) raise class BashSession: def __init__(self, session_id: Optional[str] = None, no_agi: bool = False): """Initialize Bash session with optional existing session ID""" self.session_id = session_id or self._create_session_id() self.sessions_dir = SESSIONS_DIR self.client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY")) self.messages = [] # Initialize a persistent environment dictionary for subprocesses self.environment = os.environ.copy() # Initialize logger placeholder self.logger = None # Set log prefix self.log_prefix = "🐚 bash" # Store the no_agi flag self.no_agi = no_agi def set_logger(self, session_logger: SessionLogger): """Set the logger for the session and store the SessionLogger instance.""" self.session_logger = session_logger self.logger = logging.LoggerAdapter( session_logger.logger, {"prefix": self.log_prefix} ) def _create_session_id(self) -> str: """Create a new session ID""" timestamp = datetime.now().strftime("%Y%m%d-%H:%M:%S-%f") # return f"{timestamp}-{uuid.uuid4().hex[:6]}" return f"{timestamp}" def _handle_bash_command(self, tool_call: Dict[str, Any]) -> Dict[str, Any]: """Handle bash command execution""" try: command = tool_call.get("command") restart = tool_call.get("restart", False) if restart: self.environment = os.environ.copy() # Reset the environment self.logger.info("Bash session restarted.") return {"content": "Bash session restarted."} if not command: self.logger.error("No command provided to execute.") return {"error": "No command provided to execute."} # Check if no_agi is enabled if self.no_agi: self.logger.info(f"Mock executing bash command: {command}") return {"content": "in mock mode, command did not run"} # Log the command being executed self.logger.info(f"Executing bash command: {command}") # Execute the command in a subprocess result = subprocess.run( command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=self.environment, text=True, executable="/bin/bash", ) output = result.stdout.strip() error_output = result.stderr.strip() # Log the outputs if output: self.logger.info( f"Command output:\n\n```output for '{command[:20]}...'\n{output}\n```" ) if error_output: self.logger.error( f"Command error output:\n\n```error for '{command}'\n{error_output}\n```" ) if result.returncode != 0: error_message = error_output or "Command execution failed." return {"error": error_message} return {"content": output} except Exception as e: self.logger.error(f"Error in _handle_bash_command: {str(e)}") self.logger.error(traceback.format_exc()) return {"error": str(e)} def process_tool_calls( self, tool_calls: List[anthropic.types.ContentBlock] ) -> List[Dict[str, Any]]: """Process tool calls and return results""" results = [] for tool_call in tool_calls: if tool_call.type == "tool_use" and tool_call.name == "bash": self.logger.info(f"Bash tool call input: {tool_call.input}") result = self._handle_bash_command(tool_call.input) # Convert result to match expected tool result format is_error = False if result.get("error"): is_error = True tool_result_content = [{"type": "text", "text": result["error"]}] else: tool_result_content = [ {"type": "text", "text": result.get("content", "")} ] results.append( { "tool_call_id": tool_call.id, "output": { "type": "tool_result", "content": tool_result_content, "tool_use_id": tool_call.id, "is_error": is_error, }, } ) return results def process_bash_command(self, bash_prompt: str) -> None: """Main method to process bash commands via the assistant""" try: # Initial message with proper content structure api_message = { "role": "user", "content": [{"type": "text", "text": bash_prompt}], } self.messages = [api_message] self.logger.info(f"User input: {api_message}") while True: response = self.client.beta.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=4096, messages=self.messages, tools=[{"type": "bash_20241022", "name": "bash"}], system=BASH_SYSTEM_PROMPT, betas=["computer-use-2024-10-22"], ) # Extract token usage from the response input_tokens = getattr(response.usage, "input_tokens", 0) output_tokens = getattr(response.usage, "output_tokens", 0) self.logger.info( f"API usage: input_tokens={input_tokens}, output_tokens={output_tokens}" ) # Update token counts in SessionLogger self.session_logger.update_token_usage(input_tokens, output_tokens) self.logger.info(f"API response: {response.model_dump()}") # Convert response content to message params response_content = [] for block in response.content: if block.type == "text": response_content.append({"type": "text", "text": block.text}) else: response_content.append(block.model_dump()) # Add assistant response to messages self.messages.append({"role": "assistant", "content": response_content}) if response.stop_reason != "tool_use": # Print the assistant's final response print(response.content[0].text) break tool_results = self.process_tool_calls(response.content) # Add tool results as user message if tool_results: self.messages.append( {"role": "user", "content": [tool_results[0]["output"]]} ) if tool_results[0]["output"]["is_error"]: self.logger.error( f"Error: {tool_results[0]['output']['content']}" ) break # After the execution loop, log the total cost self.session_logger.log_total_cost() except Exception as e: self.logger.error(f"Error in process_bash_command: {str(e)}") self.logger.error(traceback.format_exc()) raise def main(): """Main entry point""" parser = argparse.ArgumentParser() parser.add_argument("prompt", help="The prompt for Claude", nargs="?") parser.add_argument( "--mode", choices=["editor", "bash"], default="editor", help="Mode to run" ) parser.add_argument( "--no-agi", action="store_true", help="When set, commands will not be executed, but will return 'command ran'.", ) args = parser.parse_args() # Create a shared session ID session_id = datetime.now().strftime("%Y%m%d-%H%M%S") + "-" + uuid.uuid4().hex[:6] # Create a single SessionLogger instance session_logger = SessionLogger(session_id, SESSIONS_DIR) if args.mode == "editor": session = EditorSession(session_id=session_id) # Pass the logger via setter method session.set_logger(session_logger) print(f"Session ID: {session.session_id}") session.process_edit(args.prompt) elif args.mode == "bash": session = BashSession(session_id=session_id, no_agi=args.no_agi) # Pass the logger via setter method session.set_logger(session_logger) print(f"Session ID: {session.session_id}") session.process_bash_command(args.prompt) if __name__ == "__main__": main() ``` ================================================ FILE: ai_docs/fc_openai_agents.md ================================================ # OpenAI Agents SDK Documentation This file contains documentation for the OpenAI Agents SDK, scraped from the official documentation site. ## Overview The [OpenAI Agents SDK](https://github.com/openai/openai-agents-python) enables you to build agentic AI apps in a lightweight, easy-to-use package with very few abstractions. It's a production-ready upgrade of the previous experimentation for agents, [Swarm](https://github.com/openai/swarm/tree/main). The Agents SDK has a very small set of primitives: - **Agents**, which are LLMs equipped with instructions and tools - **Handoffs**, which allow agents to delegate to other agents for specific tasks - **Guardrails**, which enable the inputs to agents to be validated In combination with Python, these primitives are powerful enough to express complex relationships between tools and agents, and allow you to build real-world applications without a steep learning curve. In addition, the SDK comes with built-in **tracing** that lets you visualize and debug your agentic flows, as well as evaluate them and even fine-tune models for your application. ### Why use the Agents SDK The SDK has two driving design principles: 1. Enough features to be worth using, but few enough primitives to make it quick to learn. 2. Works great out of the box, but you can customize exactly what happens. Here are the main features of the SDK: - Agent loop: Built-in agent loop that handles calling tools, sending results to the LLM, and looping until the LLM is done. - Python-first: Use built-in language features to orchestrate and chain agents, rather than needing to learn new abstractions. - Handoffs: A powerful feature to coordinate and delegate between multiple agents. - Guardrails: Run input validations and checks in parallel to your agents, breaking early if the checks fail. - Function tools: Turn any Python function into a tool, with automatic schema generation and Pydantic-powered validation. - Tracing: Built-in tracing that lets you visualize, debug and monitor your workflows, as well as use the OpenAI suite of evaluation, fine-tuning and distillation tools. ### Installation ```bash pip install openai-agents ``` ### Hello world example ```python from agents import Agent, Runner agent = Agent(name="Assistant", instructions="You are a helpful assistant") result = Runner.run_sync(agent, "Write a haiku about recursion in programming.") print(result.final_output) # Code within the code, # Functions calling themselves, # Infinite loop's dance. ``` ## Quickstart ### Create a project and virtual environment ```bash mkdir my_project cd my_project python -m venv .venv source .venv/bin/activate pip install openai-agents export OPENAI_API_KEY=sk-... ``` ### Create your first agent ```python from agents import Agent agent = Agent( name="Math Tutor", instructions="You provide help with math problems. Explain your reasoning at each step and include examples", ) ``` ### Add a few more agents ```python from agents import Agent history_tutor_agent = Agent( name="History Tutor", handoff_description="Specialist agent for historical questions", instructions="You provide assistance with historical queries. Explain important events and context clearly.", ) math_tutor_agent = Agent( name="Math Tutor", handoff_description="Specialist agent for math questions", instructions="You provide help with math problems. Explain your reasoning at each step and include examples", ) ``` ### Define your handoffs ```python triage_agent = Agent( name="Triage Agent", instructions="You determine which agent to use based on the user's homework question", handoffs=[history_tutor_agent, math_tutor_agent] ) ``` ### Run the agent orchestration ```python from agents import Runner async def main(): result = await Runner.run(triage_agent, "What is the capital of France?") print(result.final_output) ``` ### Add a guardrail ```python from agents import GuardrailFunctionOutput, Agent, Runner from pydantic import BaseModel class HomeworkOutput(BaseModel): is_homework: bool reasoning: str guardrail_agent = Agent( name="Guardrail check", instructions="Check if the user is asking about homework.", output_type=HomeworkOutput, ) async def homework_guardrail(ctx, agent, input_data): result = await Runner.run(guardrail_agent, input_data, context=ctx.context) final_output = result.final_output_as(HomeworkOutput) return GuardrailFunctionOutput( output_info=final_output, tripwire_triggered=not final_output.is_homework, ) ``` ### Put it all together ```python from agents import Agent, InputGuardrail,GuardrailFunctionOutput, Runner from pydantic import BaseModel import asyncio class HomeworkOutput(BaseModel): is_homework: bool reasoning: str guardrail_agent = Agent( name="Guardrail check", instructions="Check if the user is asking about homework.", output_type=HomeworkOutput, ) math_tutor_agent = Agent( name="Math Tutor", handoff_description="Specialist agent for math questions", instructions="You provide help with math problems. Explain your reasoning at each step and include examples", ) history_tutor_agent = Agent( name="History Tutor", handoff_description="Specialist agent for historical questions", instructions="You provide assistance with historical queries. Explain important events and context clearly.", ) async def homework_guardrail(ctx, agent, input_data): result = await Runner.run(guardrail_agent, input_data, context=ctx.context) final_output = result.final_output_as(HomeworkOutput) return GuardrailFunctionOutput( output_info=final_output, tripwire_triggered=not final_output.is_homework, ) triage_agent = Agent( name="Triage Agent", instructions="You determine which agent to use based on the user's homework question", handoffs=[history_tutor_agent, math_tutor_agent], input_guardrails=[ InputGuardrail(guardrail_function=homework_guardrail), ], ) async def main(): result = await Runner.run(triage_agent, "who was the first president of the united states?") print(result.final_output) result = await Runner.run(triage_agent, "what is life") print(result.final_output) if __name__ == "__main__": asyncio.run(main()) ``` ## Agents Agents are the core building block in your apps. An agent is a large language model (LLM), configured with instructions and tools. ### Basic configuration The most common properties of an agent you'll configure are: - `instructions`: also known as a developer message or system prompt. - `model`: which LLM to use, and optional `model_settings` to configure model tuning parameters like temperature, top_p, etc. - `tools`: Tools that the agent can use to achieve its tasks. ```python from agents import Agent, ModelSettings, function_tool @function_tool def get_weather(city: str) -> str: return f"The weather in {city} is sunny" agent = Agent( name="Haiku agent", instructions="Always respond in haiku form", model="o3-mini", tools=[get_weather], ) ``` ### Context Agents are generic on their `context` type. Context is a dependency-injection tool: it's an object you create and pass to `Runner.run()`, that is passed to every agent, tool, handoff etc, and it serves as a grab bag of dependencies and state for the agent run. You can provide any Python object as the context. ### Output types By default, agents produce plain text (i.e. `str`) outputs. If you want the agent to produce a particular type of output, you can use the `output_type` parameter. ### Handoffs Handoffs are sub-agents that the agent can delegate to. You provide a list of handoffs, and the agent can choose to delegate to them if relevant. ### Dynamic instructions In most cases, you can provide instructions when you create the agent. However, you can also provide dynamic instructions via a function. ### Lifecycle events (hooks) Sometimes, you want to observe the lifecycle of an agent. For example, you may want to log events, or pre-fetch data when certain events occur. ### Guardrails Guardrails allow you to run checks/validations on user input, in parallel to the agent running. ### Cloning/copying agents By using the `clone()` method on an agent, you can duplicate an Agent, and optionally change any properties you like. ## Handoffs Handoffs allow an agent to delegate tasks to another agent. This is particularly useful in scenarios where different agents specialize in distinct areas. ### Creating a handoff All agents have a `handoffs` param, which can either take an `Agent` directly, or a `Handoff` object that customizes the Handoff. ### Basic Usage ```python from agents import Agent, handoff billing_agent = Agent(name="Billing agent") refund_agent = Agent(name="Refund agent") triage_agent = Agent(name="Triage agent", handoffs=[billing_agent, handoff(refund_agent)]) ``` ### Customizing handoffs The `handoff()` function lets you customize various aspects like tool name, description, callbacks, and input filtering. ### Handoff inputs You can have the LLM provide data when calling a handoff, which is useful for logging or other purposes. ### Input filters When a handoff occurs, the new agent sees the entire previous conversation history by default. Input filters allow you to modify this behavior. ### Recommended prompts To ensure LLMs understand handoffs properly, include information about handoffs in your agent instructions. ## Tools Tools let agents take actions: things like fetching data, running code, calling external APIs, and even using a computer. There are three classes of tools in the Agent SDK: - Hosted tools: run on LLM servers alongside the AI models - Function calling: allow you to use any Python function as a tool - Agents as tools: allow you to use an agent as a tool ### Hosted tools OpenAI offers built-in tools like `WebSearchTool`, `FileSearchTool`, and `ComputerTool`. ### Function tools You can use any Python function as a tool. The Agents SDK will automatically set up the tool with appropriate name, description and schema. ```python import json from typing_extensions import TypedDict from agents import Agent, FunctionTool, RunContextWrapper, function_tool class Location(TypedDict): lat: float long: float @function_tool async def fetch_weather(location: Location) -> str: """Fetch the weather for a given location. Args: location: The location to fetch the weather for. """ # In real life, we'd fetch the weather from a weather API return "sunny" @function_tool(name_override="fetch_data") def read_file(ctx: RunContextWrapper[Any], path: str, directory: str | None = None) -> str: """Read the contents of a file.""" # In real life, we'd read the file from the file system return "" ``` ### Agents as tools In some workflows, you may want a central agent to orchestrate a network of specialized agents, instead of handing off control. ### Handling errors in function tools You can customize error handling for function tools using the `failure_error_function` parameter. ## Results When you call the `Runner.run` methods, you get either a `RunResult` or `RunResultStreaming` object containing information about the agent run. ### Final output The `final_output` property contains the final output of the last agent that ran. ### Inputs for the next turn You can use `result.to_input_list()` to turn the result into an input list that concatenates the original input you provided with items generated during the agent run. ### Last agent The `last_agent` property contains the last agent that ran, which can be useful for subsequent user interactions. ### New items The `new_items` property contains the new items generated during the run, including messages, tool calls, handoffs, etc. ## Running agents You can run agents via the `Runner` class with three options: 1. `Runner.run()` - async method returning a `RunResult` 2. `Runner.run_sync()` - sync wrapper around `run()` 3. `Runner.run_streamed()` - async method that streams LLM events as they occur ### The agent loop When you use the run method, the runner executes a loop: 1. Call the LLM for the current agent with the current input 2. Process the LLM output: - If it's a final output, end the loop and return the result - If it's a handoff, update the current agent and input, and re-run the loop - If it's tool calls, run the tools, append results, and re-run the loop 3. If max_turns is exceeded, raise an exception ### Run config The `run_config` parameter lets you configure various global settings for the agent run. ### Conversations/chat threads Each run represents a single logical turn in a chat conversation. You can use `RunResultBase.to_input_list()` to get inputs for the next turn. ## Tracing The Agents SDK includes built-in tracing, collecting a comprehensive record of events during an agent run: LLM generations, tool calls, handoffs, guardrails, and custom events. ### Traces and spans - **Traces** represent a single end-to-end operation of a "workflow" - **Spans** represent operations that have a start and end time ### Default tracing By default, the SDK traces the entire run, each agent execution, LLM generations, function tool calls, guardrails, and handoffs. ### Higher level traces Sometimes, you might want multiple calls to `run()` to be part of a single trace: ```python from agents import Agent, Runner, trace async def main(): agent = Agent(name="Joke generator", instructions="Tell funny jokes.") with trace("Joke workflow"): first_result = await Runner.run(agent, "Tell me a joke") second_result = await Runner.run(agent, f"Rate this joke: {first_result.final_output}") print(f"Joke: {first_result.final_output}") print(f"Rating: {second_result.final_output}") ``` ### Custom trace processors You can customize tracing to send traces to alternative or additional backends: 1. `add_trace_processor()` adds an additional processor alongside the default one 2. `set_trace_processors()` replaces the default processor entirely ## Context Management Context is an overloaded term with two main aspects: 1. **Local context**: Data and dependencies available to your code during tool function execution, callbacks, lifecycle hooks, etc. 2. **LLM context**: Data the LLM sees when generating a response ### Local context This is represented via the `RunContextWrapper` class and allows you to pass any Python object to be available throughout the agent run: ```python import asyncio from dataclasses import dataclass from agents import Agent, RunContextWrapper, Runner, function_tool @dataclass class UserInfo: name: str uid: int @function_tool async def fetch_user_age(wrapper: RunContextWrapper[UserInfo]) -> str: return f"User {wrapper.context.name} is 47 years old" async def main(): user_info = UserInfo(name="John", uid=123) agent = Agent[UserInfo]( name="Assistant", tools=[fetch_user_age], ) result = await Runner.run( starting_agent=agent, input="What is the age of the user?", context=user_info, ) print(result.final_output) # The user John is 47 years old. ``` ### Agent/LLM context When an LLM is called, it can only see data from the conversation history. There are several ways to make data available: 1. Add it to the Agent `instructions` (system prompt) 2. Add it to the `input` when calling `Runner.run` 3. Expose it via function tools for on-demand access 4. Use retrieval or web search tools to fetch relevant contextual data ## Model Context Protocol (MCP) The [Model Context Protocol](https://modelcontextprotocol.io/introduction) (aka MCP) is a way to provide tools and context to the LLM. MCP provides a standardized way to connect AI models to different data sources and tools. ### MCP Servers The Agents SDK supports two types of MCP servers: 1. **stdio servers** run as a subprocess of your application (locally) 2. **HTTP over SSE servers** run remotely (connect via URL) You can use `MCPServerStdio` and `MCPServerSse` classes to connect to these servers: ```python from agents.mcp.server import MCPServerStdio, MCPServerSse # Example using the filesystem MCP server async with MCPServerStdio( params={ "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", samples_dir], } ) as server: tools = await server.list_tools() ``` ### Using MCP Servers with Agents MCP servers can be added directly to Agents: ```python agent = Agent( name="Assistant", instructions="Use the tools to achieve the task", mcp_servers=[mcp_server_1, mcp_server_2] ) ``` When the Agent runs, it will automatically call `list_tools()` on all MCP servers, making the LLM aware of all available tools. When the LLM calls a tool from an MCP server, the SDK handles calling `call_tool()` on that server. ### Caching Tool Lists For better performance, especially with remote servers, you can cache the list of tools: ```python mcp_server = MCPServerSse( url="https://example.com/mcp", cache_tools_list=True # Enable caching ) # Later, if needed, clear the cache mcp_server.invalidate_tools_cache() ``` Only use caching when you're certain the tool list will not change during execution. ### Tracing MCP Operations The Agents SDK's tracing system automatically captures MCP operations, including: 1. Calls to MCP servers to list tools 2. MCP-related information on function calls This makes it easier to debug and analyze your agent's interactions with MCP tools. ### Use a different LLM ```python import asyncio import os from openai import AsyncOpenAI from agents import Agent, OpenAIChatCompletionsModel, Runner, function_tool, set_tracing_disabled BASE_URL = os.getenv("EXAMPLE_BASE_URL") or "" API_KEY = os.getenv("EXAMPLE_API_KEY") or "" MODEL_NAME = os.getenv("EXAMPLE_MODEL_NAME") or "" if not BASE_URL or not API_KEY or not MODEL_NAME: raise ValueError( "Please set EXAMPLE_BASE_URL, EXAMPLE_API_KEY, EXAMPLE_MODEL_NAME via env var or code." ) """This example uses a custom provider for a specific agent. Steps: 1. Create a custom OpenAI client. 2. Create a `Model` that uses the custom client. 3. Set the `model` on the Agent. Note that in this example, we disable tracing under the assumption that you don't have an API key from platform.openai.com. If you do have one, you can either set the `OPENAI_API_KEY` env var or call set_tracing_export_api_key() to set a tracing specific key. """ client = AsyncOpenAI(base_url=BASE_URL, api_key=API_KEY) set_tracing_disabled(disabled=True) # An alternate approach that would also work: # PROVIDER = OpenAIProvider(openai_client=client) # agent = Agent(..., model="some-custom-model") # Runner.run(agent, ..., run_config=RunConfig(model_provider=PROVIDER)) @function_tool def get_weather(city: str): print(f"[debug] getting weather for {city}") return f"The weather in {city} is sunny." async def main(): # This agent will use the custom LLM provider agent = Agent( name="Assistant", instructions="You only respond in haikus.", model=OpenAIChatCompletionsModel(model=MODEL_NAME, openai_client=client), tools=[get_weather], ) result = await Runner.run(agent, "What's the weather in Tokyo?") print(result.final_output) if __name__ == "__main__": asyncio.run(main()) ``` ================================================ FILE: ai_docs/openai-function-calling.md ================================================ Log in [Sign up](https://platform.openai.com/signup) # Function calling Copy page Enable models to fetch data and take actions. **Function calling** provides a powerful and flexible way for OpenAI models to interface with your code or external services, and has two primary use cases: | | | | --- | --- | | **Fetching Data** | Retrieve up-to-date information to incorporate into the model's response (RAG). Useful for searching knowledge bases and retrieving specific data from APIs (e.g. current weather data). | | **Taking Action** | Perform actions like submitting a form, calling APIs, modifying application state (UI/frontend or backend), or taking agentic workflow actions (like [handing off](https://cookbook.openai.com/examples/orchestrating_agents) the conversation). | If you only want the model to produce JSON, see our docs on [structured outputs](https://platform.openai.com/docs/guides/structured-outputs). Get weatherGet weatherSend emailSend emailSearch knowledge baseSearch knowledge base Get weather Function calling example with get\_weather function python ```python 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 from openai import OpenAI client = OpenAI() tools = [{\ "type": "function",\ "function": {\ "name": "get_weather",\ "description": "Get current temperature for a given location.",\ "parameters": {\ "type": "object",\ "properties": {\ "location": {\ "type": "string",\ "description": "City and country e.g. Bogotá, Colombia"\ }\ },\ "required": [\ "location"\ ],\ "additionalProperties": False\ },\ "strict": True\ }\ }] completion = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "What is the weather like in Paris today?"}], tools=tools ) print(completion.choices[0].message.tool_calls) ``` ```javascript 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import { OpenAI } from "openai"; const openai = new OpenAI(); const tools = [{\ "type": "function",\ "function": {\ "name": "get_weather",\ "description": "Get current temperature for a given location.",\ "parameters": {\ "type": "object",\ "properties": {\ "location": {\ "type": "string",\ "description": "City and country e.g. Bogotá, Colombia"\ }\ },\ "required": [\ "location"\ ],\ "additionalProperties": false\ },\ "strict": true\ }\ }]; const completion = await openai.chat.completions.create({ model: "gpt-4o", messages: [{ role: "user", content: "What is the weather like in Paris today?" }], tools, store: true, }); console.log(completion.choices[0].message.tool_calls); ``` ```bash 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 curl https://api.openai.com/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -d '{ "model": "gpt-4o", "messages": [\ {\ "role": "user",\ "content": "What is the weather like in Paris today?"\ }\ ], "tools": [\ {\ "type": "function",\ "function": {\ "name": "get_weather",\ "description": "Get current temperature for a given location.",\ "parameters": {\ "type": "object",\ "properties": {\ "location": {\ "type": "string",\ "description": "City and country e.g. Bogotá, Colombia"\ }\ },\ "required": [\ "location"\ ],\ "additionalProperties": false\ },\ "strict": true\ }\ }\ ] }' ``` Output ```json 1 2 3 4 5 6 7 8 [{\ "id": "call_12345xyz",\ "type": "function",\ "function": {\ "name": "get_weather",\ "arguments": "{\"location\":\"Paris, France\"}"\ }\ }] ``` Send email Function calling example with send\_email function python ```python 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 from openai import OpenAI client = OpenAI() tools = [{\ "type": "function",\ "function": {\ "name": "send_email",\ "description": "Send an email to a given recipient with a subject and message.",\ "parameters": {\ "type": "object",\ "properties": {\ "to": {\ "type": "string",\ "description": "The recipient email address."\ },\ "subject": {\ "type": "string",\ "description": "Email subject line."\ },\ "body": {\ "type": "string",\ "description": "Body of the email message."\ }\ },\ "required": [\ "to",\ "subject",\ "body"\ ],\ "additionalProperties": False\ },\ "strict": True\ }\ }] completion = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "Can you send an email to ilan@example.com and katia@example.com saying hi?"}], tools=tools ) print(completion.choices[0].message.tool_calls) ``` ```javascript 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import { OpenAI } from "openai"; const openai = new OpenAI(); const tools = [{\ "type": "function",\ "function": {\ "name": "send_email",\ "description": "Send an email to a given recipient with a subject and message.",\ "parameters": {\ "type": "object",\ "properties": {\ "to": {\ "type": "string",\ "description": "The recipient email address."\ },\ "subject": {\ "type": "string",\ "description": "Email subject line."\ },\ "body": {\ "type": "string",\ "description": "Body of the email message."\ }\ },\ "required": [\ "to",\ "subject",\ "body"\ ],\ "additionalProperties": false\ },\ "strict": true\ }\ }]; const completion = await openai.chat.completions.create({ model: "gpt-4o", messages: [{ role: "user", content: "Can you send an email to ilan@example.com and katia@example.com saying hi?" }], tools, store: true, }); console.log(completion.choices[0].message.tool_calls); ``` ```bash 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 curl https://api.openai.com/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -d '{ "model": "gpt-4o", "messages": [\ {\ "role": "user",\ "content": "Can you send an email to ilan@example.com and katia@example.com saying hi?"\ }\ ], "tools": [\ {\ "type": "function",\ "function": {\ "name": "send_email",\ "description": "Send an email to a given recipient with a subject and message.",\ "parameters": {\ "type": "object",\ "properties": {\ "to": {\ "type": "string",\ "description": "The recipient email address."\ },\ "subject": {\ "type": "string",\ "description": "Email subject line."\ },\ "body": {\ "type": "string",\ "description": "Body of the email message."\ }\ },\ "required": [\ "to",\ "subject",\ "body"\ ],\ "additionalProperties": false\ },\ "strict": true\ }\ }\ ] }' ``` Output ```json 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [\ {\ "id": "call_9876abc",\ "type": "function",\ "function": {\ "name": "send_email",\ "arguments": "{\"to\":\"ilan@example.com\",\"subject\":\"Hello!\",\"body\":\"Just wanted to say hi\"}"\ }\ },\ {\ "id": "call_9876abc",\ "type": "function",\ "function": {\ "name": "send_email",\ "arguments": "{\"to\":\"katia@example.com\",\"subject\":\"Hello!\",\"body\":\"Just wanted to say hi\"}"\ }\ }\ ] ``` Search knowledge base Function calling example with search\_knowledge\_base function python ```python 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 from openai import OpenAI client = OpenAI() tools = [{\ "type": "function",\ "function": {\ "name": "search_knowledge_base",\ "description": "Query a knowledge base to retrieve relevant info on a topic.",\ "parameters": {\ "type": "object",\ "properties": {\ "query": {\ "type": "string",\ "description": "The user question or search query."\ },\ "options": {\ "type": "object",\ "properties": {\ "num_results": {\ "type": "number",\ "description": "Number of top results to return."\ },\ "domain_filter": {\ "type": [\ "string",\ "null"\ ],\ "description": "Optional domain to narrow the search (e.g. 'finance', 'medical'). Pass null if not needed."\ },\ "sort_by": {\ "type": [\ "string",\ "null"\ ],\ "enum": [\ "relevance",\ "date",\ "popularity",\ "alphabetical"\ ],\ "description": "How to sort results. Pass null if not needed."\ }\ },\ "required": [\ "num_results",\ "domain_filter",\ "sort_by"\ ],\ "additionalProperties": False\ }\ },\ "required": [\ "query",\ "options"\ ],\ "additionalProperties": False\ },\ "strict": True\ }\ }] completion = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "Can you find information about ChatGPT in the AI knowledge base?"}], tools=tools ) print(completion.choices[0].message.tool_calls) ``` ```javascript 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 import { OpenAI } from "openai"; const openai = new OpenAI(); const tools = [{\ "type": "function",\ "function": {\ "name": "search_knowledge_base",\ "description": "Query a knowledge base to retrieve relevant info on a topic.",\ "parameters": {\ "type": "object",\ "properties": {\ "query": {\ "type": "string",\ "description": "The user question or search query."\ },\ "options": {\ "type": "object",\ "properties": {\ "num_results": {\ "type": "number",\ "description": "Number of top results to return."\ },\ "domain_filter": {\ "type": [\ "string",\ "null"\ ],\ "description": "Optional domain to narrow the search (e.g. 'finance', 'medical'). Pass null if not needed."\ },\ "sort_by": {\ "type": [\ "string",\ "null"\ ],\ "enum": [\ "relevance",\ "date",\ "popularity",\ "alphabetical"\ ],\ "description": "How to sort results. Pass null if not needed."\ }\ },\ "required": [\ "num_results",\ "domain_filter",\ "sort_by"\ ],\ "additionalProperties": false\ }\ },\ "required": [\ "query",\ "options"\ ],\ "additionalProperties": false\ },\ "strict": true\ }\ }]; const completion = await openai.chat.completions.create({ model: "gpt-4o", messages: [{ role: "user", content: "Can you find information about ChatGPT in the AI knowledge base?" }], tools, store: true, }); console.log(completion.choices[0].message.tool_calls); ``` ```bash 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 curl https://api.openai.com/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -d '{ "model": "gpt-4o", "messages": [\ {\ "role": "user",\ "content": "Can you find information about ChatGPT in the AI knowledge base?"\ }\ ], "tools": [\ {\ "type": "function",\ "function": {\ "name": "search_knowledge_base",\ "description": "Query a knowledge base to retrieve relevant info on a topic.",\ "parameters": {\ "type": "object",\ "properties": {\ "query": {\ "type": "string",\ "description": "The user question or search query."\ },\ "options": {\ "type": "object",\ "properties": {\ "num_results": {\ "type": "number",\ "description": "Number of top results to return."\ },\ "domain_filter": {\ "type": [\ "string",\ "null"\ ],\ "description": "Optional domain to narrow the search (e.g. 'finance', 'medical'). Pass null if not needed."\ },\ "sort_by": {\ "type": [\ "string",\ "null"\ ],\ "enum": [\ "relevance",\ "date",\ "popularity",\ "alphabetical"\ ],\ "description": "How to sort results. Pass null if not needed."\ }\ },\ "required": [\ "num_results",\ "domain_filter",\ "sort_by"\ ],\ "additionalProperties": false\ }\ },\ "required": [\ "query",\ "options"\ ],\ "additionalProperties": false\ },\ "strict": true\ }\ }\ ] }' ``` Output ```json 1 2 3 4 5 6 7 8 [{\ "id": "call_4567xyz",\ "type": "function",\ "function": {\ "name": "search_knowledge_base",\ "arguments": "{\"query\":\"What is ChatGPT?\",\"options\":{\"num_results\":3,\"domain_filter\":null,\"sort_by\":\"relevance\"}}"\ }\ }] ``` Experiment with function calling and [generate function schemas](https://platform.openai.com/docs/guides/prompt-generation) in the [Playground](https://platform.openai.com/playground)! ## Overview You can extend the capabilities of OpenAI models by giving them access to `tools`, which can have one of two forms: | | | | --- | --- | | **Function Calling** | Developer-defined code. | | **Hosted Tools** | OpenAI-built tools. ( _e.g. file search, code interpreter_)
Only available in the [Assistants API](https://platform.openai.com/docs/assistants/tools). | This guide will cover how you can give the model access to your own functions through **function calling**. Based on the system prompt and messages, the model may decide to call these functions — **instead of (or in addition to) generating text or audio**. You'll then execute the function code, send back the results, and the model will incorporate them into its final response. ![Function Calling Diagram Steps](https://cdn.openai.com/API/docs/images/function-calling-diagram-steps.png) ### Sample function Let's look at the steps to allow a model to use a real `get_weather` function defined below: Sample get\_weather function implemented in your codebase python ```python 1 2 3 4 5 6 import requests def get_weather(latitude, longitude): response = requests.get(f"https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}¤t=temperature_2m,wind_speed_10m&hourly=temperature_2m,relative_humidity_2m,wind_speed_10m") data = response.json() return data['current']['temperature_2m'] ``` ```javascript 1 2 3 4 5 async function getWeather(latitude, longitude) { const response = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,wind_speed_10m&hourly=temperature_2m,relative_humidity_2m,wind_speed_10m`); const data = await response.json(); return data.current.temperature_2m; } ``` Unlike the diagram earlier, this function expects precise `latitude` and `longitude` instead of a general `location` parameter. (However, our models can automatically determine the coordinates for many locations!) ### Function calling steps **Call model with [functions defined](https://platform.openai.com/docs/guides/function-calling#defining-functions)** – along with your system and user messages. Step 1: Call model with get\_weather tool defined python ```python 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 from openai import OpenAI import json client = OpenAI() tools = [{\ "type": "function",\ "function": {\ "name": "get_weather",\ "description": "Get current temperature for provided coordinates in celsius.",\ "parameters": {\ "type": "object",\ "properties": {\ "latitude": {"type": "number"},\ "longitude": {"type": "number"}\ },\ "required": ["latitude", "longitude"],\ "additionalProperties": False\ },\ "strict": True\ }\ }] messages = [{"role": "user", "content": "What's the weather like in Paris today?"}] completion = client.chat.completions.create( model="gpt-4o", messages=messages, tools=tools, ) ``` ```javascript 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import { OpenAI } from "openai"; const openai = new OpenAI(); const tools = [{\ type: "function",\ function: {\ name: "get_weather",\ description: "Get current temperature for provided coordinates in celsius.",\ parameters: {\ type: "object",\ properties: {\ latitude: { type: "number" },\ longitude: { type: "number" }\ },\ required: ["latitude", "longitude"],\ additionalProperties: false\ },\ strict: true\ }\ }]; const messages = [\ {\ role: "user",\ content: "What's the weather like in Paris today?"\ }\ ]; const completion = await openai.chat.completions.create({ model: "gpt-4o", messages, tools, store: true, }); ``` **Model decides to call function(s)** – model returns the **name** and **input arguments**. completion.choices\[0\].message.tool\_calls ```json 1 2 3 4 5 6 7 8 [{\ "id": "call_12345xyz",\ "type": "function",\ "function": {\ "name": "get_weather",\ "arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}"\ }\ }] ``` **Execute function code** – parse the model's response and [handle function calls](https://platform.openai.com/docs/guides/function-calling#handling-function-calls). Step 3: Execute get\_weather function python ```python 1 2 3 4 tool_call = completion.choices[0].message.tool_calls[0] args = json.loads(tool_call.function.arguments) result = get_weather(args["latitude"], args["longitude"]) ``` ```javascript 1 2 3 4 const toolCall = completion.choices[0].message.tool_calls[0]; const args = JSON.parse(toolCall.function.arguments); const result = await get_weather(args.latitude, args.longitude); ``` **Supply model with results** – so it can incorporate them into its final response. Step 4: Supply result and call model again python ```python 1 2 3 4 5 6 7 8 9 10 11 12 messages.append(completion.choices[0].message) # append model's function call message messages.append({ # append result message "role": "tool", "tool_call_id": tool_call.id, "content": str(result) }) completion_2 = client.chat.completions.create( model="gpt-4o", messages=messages, tools=tools, ) ``` ```javascript 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 messages.push(completion.choices[0].message); // append model's function call message messages.push({ // append result message role: "tool", tool_call_id: toolCall.id, content: result.toString() }); const completion2 = await openai.chat.completions.create({ model: "gpt-4o", messages, tools, store: true, }); console.log(completion2.choices[0].message.content); ``` **Model responds** – incorporating the result in its output. completion\_2.choices\[0\].message.content ```json "The current temperature in Paris is 14°C (57.2°F)." ``` ## Defining functions Functions can be set in the `tools` parameter of each API request inside a `function` object. A function is defined by its schema, which informs the model what it does and what input arguments it expects. It comprises the following fields: | Field | Description | | --- | --- | | `name` | The function's name (e.g. `get_weather`) | | `description` | Details on when and how to use the function | | `parameters` | [JSON schema](https://json-schema.org/) defining the function's input arguments | Take a look at this example or generate your own below (or in our [Playground](https://platform.openai.com/playground)). Generate Example function schema ```json 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 { "type": "function", "function": { "name": "get_weather", "description": "Retrieves current weather for the given location.", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "City and country e.g. Bogotá, Colombia" }, "units": { "type": "string", "enum": [\ "celsius",\ "fahrenheit"\ ], "description": "Units the temperature will be returned in." } }, "required": [\ "location",\ "units"\ ], "additionalProperties": false }, "strict": true } } ``` Because the `parameters` are defined by a [JSON schema](https://json-schema.org/), you can leverage many of its rich features like property types, enums, descriptions, nested objects, and, recursive objects. (Optional) Function calling wth pydantic and zod While we encourage you to define your function schemas directly, our SDKs have helpers to convert `pydantic` and `zod` objects into schemas. Not all `pydantic` and `zod` features are supported. Define objects to represent function schema python ```python 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from openai import OpenAI, pydantic_function_tool from pydantic import BaseModel, Field client = OpenAI() class GetWeather(BaseModel): location: str = Field( ..., description="City and country e.g. Bogotá, Colombia" ) tools = [pydantic_function_tool(GetWeather)] completion = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "What's the weather like in Paris today?"}], tools=tools ) print(completion.choices[0].message.tool_calls) ``` ```javascript 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import OpenAI from "openai"; import { z } from "zod"; import { zodFunction } from "openai/helpers/zod"; const openai = new OpenAI(); const GetWeatherParameters = z.object({ location: z.string().describe("City and country e.g. Bogotá, Colombia"), }); const tools = [\ zodFunction({ name: "getWeather", parameters: GetWeatherParameters }),\ ]; const messages = [\ { role: "user", content: "What's the weather like in Paris today?" },\ ]; const response = await openai.chat.completions.create({ model: "gpt-4o", messages, tools, store: true, }); console.log(response.choices[0].message.tool_calls); ``` ### Best practices for defining functions 1. **Write clear and detailed function names, parameter descriptions, and instructions.** - **Explicitly describe the purpose of the function and each parameter** (and its format), and what the output represents. - **Use the system prompt to describe when (and when not) to use each function.** Generally, tell the model _exactly_ what to do. - **Include examples and edge cases**, especially to rectify any recurring failures. ( **Note:** Adding examples may hurt performance for [reasoning models](https://platform.openai.com/docs/guides/reasoning).) 2. **Apply software engineering best practices.** - **Make the functions obvious and intuitive**. ( [principle of least surprise](https://en.wikipedia.org/wiki/Principle_of_least_astonishment)) - **Use enums** and object structure to make invalid states unrepresentable. (e.g. `toggle_light(on: bool, off: bool)` allows for invalid calls) - **Pass the intern test.** Can an intern/human correctly use the function given nothing but what you gave the model? (If not, what questions do they ask you? Add the answers to the prompt.) 3. **Offload the burden from the model and use code where possible.** - **Don't make the model fill arguments you already know.** For example, if you already have an `order_id` based on a previous menu, don't have an `order_id` param – instead, have no params `submit_refund()` and pass the `order_id` with code. - **Combine functions that are always called in sequence.** For example, if you always call `mark_location()` after `query_location()`, just move the marking logic into the query function call. 4. **Keep the number of functions small for higher accuracy.** - **Evaluate your performance** with different numbers of functions. - **Aim for fewer than 20 functions** at any one time, though this is just a soft suggestion. 5. **Leverage OpenAI resources.** - **Generate and iterate on function schemas** in the [Playground](https://platform.openai.com/playground). - **Consider [fine-tuning](https://platform.openai.com/docs/guides/fine-tuning) to increase function calling accuracy** for large numbers of functions or difficult tasks. ( [cookbook](https://cookbook.openai.com/examples/fine_tuning_for_function_calling)) ### Token Usage Under the hood, functions are injected into the system message in a syntax the model has been trained on. This means functions count against the model's context limit and are billed as input tokens. If you run into token limits, we suggest limiting the number of functions or the length of the descriptions you provide for function parameters. It is also possible to use [fine-tuning](https://platform.openai.com/docs/guides/fine-tuning#fine-tuning-examples) to reduce the number of tokens used if you have many functions defined in your tools specification. ## Handling function calls When the model calls a function, you must execute it and return the result. Since model responses can include zero, one, or multiple calls, it is best practice to assume there are several. The response has an array of `tool_calls`, each with an `id` (used later to submit the function result) and a `function` containing a `name` and JSON-encoded `arguments`. Sample response with multiple function calls ```json 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 [\ {\ "id": "call_12345xyz",\ "type": "function",\ "function": {\ "name": "get_weather",\ "arguments": "{\"location\":\"Paris, France\"}"\ }\ },\ {\ "id": "call_67890abc",\ "type": "function",\ "function": {\ "name": "get_weather",\ "arguments": "{\"location\":\"Bogotá, Colombia\"}"\ }\ },\ {\ "id": "call_99999def",\ "type": "function",\ "function": {\ "name": "send_email",\ "arguments": "{\"to\":\"bob@email.com\",\"body\":\"Hi bob\"}"\ }\ }\ ] ``` Execute function calls and append results python ```python 1 2 3 4 5 6 7 8 9 10 for tool_call in completion.choices[0].message.tool_calls: name = tool_call.function.name args = json.loads(tool_call.function.arguments) result = call_function(name, args) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result }) ``` ```javascript 1 2 3 4 5 6 7 8 9 10 11 for (const toolCall of completion.choices[0].message.tool_calls) { const name = toolCall.function.name; const args = JSON.parse(toolCall.function.arguments); const result = callFunction(name, args); messages.push({ role: "tool", tool_call_id: toolCall.id, content: result.toString() }); } ``` In the example above, we have a hypothetical `call_function` to route each call. Here’s a possible implementation: Execute function calls and append results python ```python 1 2 3 4 5 def call_function(name, args): if name == "get_weather": return get_weather(**args) if name == "send_email": return send_email(**args) ``` ```javascript 1 2 3 4 5 6 7 8 const callFunction = async (name, args) => { if (name === "get_weather") { return getWeather(args.latitude, args.longitude); } if (name === "send_email") { return sendEmail(args.to, args.body); } }; ``` ### Formatting results A result must be a string, but the format is up to you (JSON, error codes, plain text, etc.). The model will interpret that string as needed. If your function has no return value (e.g. `send_email`), simply return a string to indicate success or failure. (e.g. `"success"`) ### Incorporating results into response After appending the results to your `messages`, you can send them back to the model to get a final response. Send results back to model python ```python 1 2 3 4 5 completion = client.chat.completions.create( model="gpt-4o", messages=messages, tools=tools, ) ``` ```javascript 1 2 3 4 5 6 const completion = await openai.chat.completions.create({ model: "gpt-4o", messages, tools, store: true, }); ``` Final response ```json "It's about 15°C in Paris, 18°C in Bogotá, and I've sent that email to Bob." ``` ## Additional configurations ### Tool choice By default the model will determine when and how many tools to use. You can force specific behavior with the `tool_choice` parameter. 1. **Auto:** ( _Default_) Call zero, one, or multiple functions. `tool_choice: "auto"` 2. **Required:** Call one or more functions. `tool_choice: "required"` 3. **Forced Function:** Call exactly one specific function. `tool_choice: {"type": "function", "function": {"name": "get_weather"}}` ![Function Calling Diagram Steps](https://cdn.openai.com/API/docs/images/function-calling-diagram-tool-choice.png) You can also set `tool_choice` to `"none"` to imitate the behavior of passing no functions. ### Parallel function calling The model may choose to call multiple functions in a single turn. You can prevent this by setting `parallel_tool_calls` to `false`, which ensures exactly zero or one tool is called. **Note:** Currently, if the model calls multiple functions in one turn then [strict mode](https://platform.openai.com/docs/guides/function-calling#strict-mode) will be disabled for those calls. ### Strict mode Setting `strict` to `true` will ensure function calls reliably adhere to the function schema, instead of being best effort. We recommend always enabling strict mode. Under the hood, strict mode works by leveraging our [structured outputs](https://platform.openai.com/docs/guides/structured-outputs) feature and therefore introduces a couple requirements: 1. `additionalProperties` must be set to `false` for each object in the `parameters`. 2. All fields in `properties` must be marked as `required`. You can denote optional fields by adding `null` as a `type` option (see example below). Strict mode enabledStrict mode enabledStrict mode disabledStrict mode disabled Strict mode enabled ```json 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 { "type": "function", "function": { "name": "get_weather", "description": "Retrieves current weather for the given location.", "strict": true, "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "City and country e.g. Bogotá, Colombia" }, "units": { "type": ["string", "null"], "enum": ["celsius", "fahrenheit"], "description": "Units the temperature will be returned in." } }, "required": ["location", "units"], "additionalProperties": false } } } ``` Strict mode disabled ```json 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 { "type": "function", "function": { "name": "get_weather", "description": "Retrieves current weather for the given location.", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "City and country e.g. Bogotá, Colombia" }, "units": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "Units the temperature will be returned in." } }, "required": ["location"], } } } ``` All schemas generated in the [playground](https://platform.openai.com/playground) have strict mode enabled. While we recommend you enable strict mode, it has a few limitations: 1. Some features of JSON schema are not supported. (See [supported schemas](https://platform.openai.com/docs/guides/structured-outputs?context=with_parse#supported-schemas).) 2. Schemas undergo additional processing on the first request (and are then cached). If your schemas vary from request to request, this may result in higher latencies. 3. Schemas are cached for performance, and are not eligible for [zero data retention](https://platform.openai.com/docs/models#how-we-use-your-data). ## Streaming Streaming can be used to surface progress by showing which function is called as the model fills its arguments, and even displaying the arguments in real time. Streaming function calls is very similar to streaming regular responses: you set `stream` to `true` and get chunks with `delta` objects. Streaming function calls python ```python 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 from openai import OpenAI client = OpenAI() tools = [{\ "type": "function",\ "function": {\ "name": "get_weather",\ "description": "Get current temperature for a given location.",\ "parameters": {\ "type": "object",\ "properties": {\ "location": {\ "type": "string",\ "description": "City and country e.g. Bogotá, Colombia"\ }\ },\ "required": ["location"],\ "additionalProperties": False\ },\ "strict": True\ }\ }] stream = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "What's the weather like in Paris today?"}], tools=tools, stream=True ) for chunk in stream: delta = chunk.choices[0].delta print(delta.tool_calls) ``` ```javascript 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import { OpenAI } from "openai"; const openai = new OpenAI(); const tools = [{\ "type": "function",\ "function": {\ "name": "get_weather",\ "description": "Get current temperature for a given location.",\ "parameters": {\ "type": "object",\ "properties": {\ "location": {\ "type": "string",\ "description": "City and country e.g. Bogotá, Colombia"\ }\ },\ "required": ["location"],\ "additionalProperties": false\ },\ "strict": true\ }\ }]; const stream = await openai.chat.completions.create({ model: "gpt-4o", messages: [{ role: "user", content: "What's the weather like in Paris today?" }], tools, stream: true, store: true, }); for await (const chunk of stream) { const delta = chunk.choices[0].delta; console.log(delta.tool_calls); } ``` Output delta.tool\_calls ```json 1 2 3 4 5 6 7 8 9 [{"index": 0, "id": "call_DdmO9pD3xa9XTPNJ32zg2hcA", "function": {"arguments": "", "name": "get_weather"}, "type": "function"}] [{"index": 0, "id": null, "function": {"arguments": "{\"", "name": null}, "type": null}] [{"index": 0, "id": null, "function": {"arguments": "location", "name": null}, "type": null}] [{"index": 0, "id": null, "function": {"arguments": "\":\"", "name": null}, "type": null}] [{"index": 0, "id": null, "function": {"arguments": "Paris", "name": null}, "type": null}] [{"index": 0, "id": null, "function": {"arguments": ",", "name": null}, "type": null}] [{"index": 0, "id": null, "function": {"arguments": " France", "name": null}, "type": null}] [{"index": 0, "id": null, "function": {"arguments": "\"}", "name": null}, "type": null}] null ``` Instead of aggregating chunks into a single `content` string, however, you're aggregating chunks into an encoded `arguments` JSON object. When the model calls one or more functions the `tool_calls` field of each `delta` will be populated. Each `tool_call` contains the following fields: | Field | Description | | --- | --- | | `index` | Identifies which function call the `delta` is for | | `id` | Tool call id. | | `function` | Function call delta ( `name` and `arguments`) | | `type` | Type of `tool_call` (always `function` for function calls) | Many of these fields are only set for the first `delta` of each tool call, like `id`, `function.name`, and `type`. Below is a code snippet demonstrating how to aggregate the `delta` s into a final `tool_calls` object. Accumulating tool\_call deltas python ```python 1 2 3 4 5 6 7 8 9 10 final_tool_calls = {} for chunk in stream: for tool_call in chunk.choices[0].delta.tool_calls or []: index = tool_call.index if index not in final_tool_calls: final_tool_calls[index] = tool_call final_tool_calls[index].function.arguments += tool_call.function.arguments ``` ```javascript 1 2 3 4 5 6 7 8 9 10 11 12 13 14 const finalToolCalls = {}; for await (const chunk of stream) { const toolCalls = chunk.choices[0].delta.tool_calls || []; for (const toolCall of toolCalls) { const { index } = toolCall; if (!finalToolCalls[index]) { finalToolCalls[index] = toolCall; } finalToolCalls[index].function.arguments += toolCall.function.arguments; } } ``` Accumulated final\_tool\_calls\[0\] ```json 1 2 3 4 5 6 7 8 { "index": 0, "id": "call_RzfkBpJgzeR0S242qfvjadNe", "function": { "name": "get_weather", "arguments": "{\"location\":\"Paris, France\"}" } } ``` ================================================ FILE: ai_docs/python_anthropic.md ================================================ [Anthropic home page![light logo](https://mintlify.s3.us-west-1.amazonaws.com/anthropic/logo/light.svg)![dark logo](https://mintlify.s3.us-west-1.amazonaws.com/anthropic/logo/dark.svg)](https://docs.anthropic.com/) English Search... Ctrl K Search... Navigation Build with Claude Tool use (function calling) [Welcome](https://docs.anthropic.com/en/home) [User Guides](https://docs.anthropic.com/en/docs/welcome) [API Reference](https://docs.anthropic.com/en/api/getting-started) [Prompt Library](https://docs.anthropic.com/en/prompt-library/library) [Release Notes](https://docs.anthropic.com/en/release-notes/overview) [Developer Newsletter](https://docs.anthropic.com/en/developer-newsletter/overview) Claude is capable of interacting with external client-side tools and functions, allowing you to equip Claude with your own custom tools to perform a wider variety of tasks. Learn everything you need to master tool use with Claude via our new comprehensive [tool use\\ course](https://github.com/anthropics/courses/tree/master/tool_use)! Please continue to share your ideas and suggestions using this [form](https://forms.gle/BFnYc6iCkWoRzFgk7). Here’s an example of how to provide tools to Claude using the Messages API: Shell Python Copy ```bash curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-3-5-sonnet-20241022", "max_tokens": 1024, "tools": [\ {\ "name": "get_weather",\ "description": "Get the current weather in a given location",\ "input_schema": {\ "type": "object",\ "properties": {\ "location": {\ "type": "string",\ "description": "The city and state, e.g. San Francisco, CA"\ }\ },\ "required": ["location"]\ }\ }\ ], "messages": [\ {\ "role": "user",\ "content": "What is the weather like in San Francisco?"\ }\ ] }' ``` * * * ## [​](https://docs.anthropic.com/en/docs/build-with-claude/tool-use\#how-tool-use-works) How tool use works Integrate external tools with Claude in these steps: 1 Provide Claude with tools and a user prompt - Define tools with names, descriptions, and input schemas in your API request. - Include a user prompt that might require these tools, e.g., “What’s the weather in San Francisco?” 2 Claude decides to use a tool - Claude assesses if any tools can help with the user’s query. - If yes, Claude constructs a properly formatted tool use request. - The API response has a `stop_reason` of `tool_use`, signaling Claude’s intent. 3 Extract tool input, run code, and return results - On your end, extract the tool name and input from Claude’s request. - Execute the actual tool code client-side. - Continue the conversation with a new `user` message containing a `tool_result` content block. 4 Claude uses tool result to formulate a response - Claude analyzes the tool results to craft its final response to the original user prompt. Note: Steps 3 and 4 are optional. For some workflows, Claude’s tool use request (step 2) might be all you need, without sending results back to Claude. **Tools are user-provided** It’s important to note that Claude does not have access to any built-in server-side tools. All tools must be explicitly provided by you, the user, in each API request. This gives you full control and flexibility over the tools Claude can use. The [computer use (beta)](https://docs.anthropic.com/en/docs/build-with-claude/computer-use) functionality is an exception - it introduces tools that are provided by Anthropic but implemented by you, the user. * * * ## [​](https://docs.anthropic.com/en/docs/build-with-claude/tool-use\#how-to-implement-tool-use) How to implement tool use ### [​](https://docs.anthropic.com/en/docs/build-with-claude/tool-use\#choosing-a-model) Choosing a model Generally, use Claude 3.5 Sonnet or Claude 3 Opus for complex tools and ambiguous queries; they handle multiple tools better and seek clarification when needed. Use Claude 3.5 Haiku or Claude 3 Haiku for straightforward tools, but note they may infer missing parameters. ### [​](https://docs.anthropic.com/en/docs/build-with-claude/tool-use\#specifying-tools) Specifying tools Tools are specified in the `tools` top-level parameter of the API request. Each tool definition includes: | Parameter | Description | | --- | --- | | `name` | The name of the tool. Must match the regex `^[a-zA-Z0-9_-]{1,64}$`. | | `description` | A detailed plaintext description of what the tool does, when it should be used, and how it behaves. | | `input_schema` | A [JSON Schema](https://json-schema.org/) object defining the expected parameters for the tool. | Example simple tool definition JSON Copy ```JSON { "name": "get_weather", "description": "Get the current weather in a given location", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA" }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "The unit of temperature, either 'celsius' or 'fahrenheit'" } }, "required": ["location"] } } ``` This tool, named `get_weather`, expects an input object with a required `location` string and an optional `unit` string that must be either “celsius” or “fahrenheit”. #### [​](https://docs.anthropic.com/en/docs/build-with-claude/tool-use\#tool-use-system-prompt) Tool use system prompt When you call the Anthropic API with the `tools` parameter, we construct a special system prompt from the tool definitions, tool configuration, and any user-specified system prompt. The constructed prompt is designed to instruct the model to use the specified tool(s) and provide the necessary context for the tool to operate properly: Copy ``` In this environment you have access to a set of tools you can use to answer the user's question. {{ FORMATTING INSTRUCTIONS }} String and scalar parameters should be specified as is, while lists and objects should use JSON format. Note that spaces for string values are not stripped. The output is not expected to be valid XML and is parsed with regular expressions. Here are the functions available in JSONSchema format: {{ TOOL DEFINITIONS IN JSON SCHEMA }} {{ USER SYSTEM PROMPT }} {{ TOOL CONFIGURATION }} ``` #### [​](https://docs.anthropic.com/en/docs/build-with-claude/tool-use\#best-practices-for-tool-definitions) Best practices for tool definitions To get the best performance out of Claude when using tools, follow these guidelines: - **Provide extremely detailed descriptions.** This is by far the most important factor in tool performance. Your descriptions should explain every detail about the tool, including: - What the tool does - When it should be used (and when it shouldn’t) - What each parameter means and how it affects the tool’s behavior - Any important caveats or limitations, such as what information the tool does not return if the tool name is unclear. The more context you can give Claude about your tools, the better it will be at deciding when and how to use them. Aim for at least 3-4 sentences per tool description, more if the tool is complex. - **Prioritize descriptions over examples.** While you can include examples of how to use a tool in its description or in the accompanying prompt, this is less important than having a clear and comprehensive explanation of the tool’s purpose and parameters. Only add examples after you’ve fully fleshed out the description. Example of a good tool description JSON Copy ```JSON { "name": "get_stock_price", "description": "Retrieves the current stock price for a given ticker symbol. The ticker symbol must be a valid symbol for a publicly traded company on a major US stock exchange like NYSE or NASDAQ. The tool will return the latest trade price in USD. It should be used when the user asks about the current or most recent price of a specific stock. It will not provide any other information about the stock or company.", "input_schema": { "type": "object", "properties": { "ticker": { "type": "string", "description": "The stock ticker symbol, e.g. AAPL for Apple Inc." } }, "required": ["ticker"] } } ``` Example poor tool description JSON Copy ```JSON { "name": "get_stock_price", "description": "Gets the stock price for a ticker.", "input_schema": { "type": "object", "properties": { "ticker": { "type": "string" } }, "required": ["ticker"] } } ``` The good description clearly explains what the tool does, when to use it, what data it returns, and what the `ticker` parameter means. The poor description is too brief and leaves Claude with many open questions about the tool’s behavior and usage. ### [​](https://docs.anthropic.com/en/docs/build-with-claude/tool-use\#controlling-claudes-output) Controlling Claude’s output #### [​](https://docs.anthropic.com/en/docs/build-with-claude/tool-use\#forcing-tool-use) Forcing tool use In some cases, you may want Claude to use a specific tool to answer the user’s question, even if Claude thinks it can provide an answer without using a tool. You can do this by specifying the tool in the `tool_choice` field like so: Copy ``` tool_choice = {"type": "tool", "name": "get_weather"} ``` When working with the tool\_choice parameter, we have three possible options: - `auto` allows Claude to decide whether to call any provided tools or not. This is the default value. - `any` tells Claude that it must use one of the provided tools, but doesn’t force a particular tool. - `tool` allows us to force Claude to always use a particular tool. This diagram illustrates how each option works: ![](https://mintlify.s3.us-west-1.amazonaws.com/anthropic/images/tool_choice.png) Note that when you have `tool_choice` as `any` or `tool`, we will prefill the assistant message to force a tool to be used. This means that the models will not emit a chain-of-thought `text` content block before `tool_use` content blocks, even if explicitly asked to do so. Our testing has shown that this should not reduce performance. If you would like to keep chain-of-thought (particularly with Opus) while still requesting that the model use a specific tool, you can use `{"type": "auto"}` for `tool_choice` (the default) and add explicit instructions in a `user` message. For example: `What's the weather like in London? Use the get_weather tool in your response.` #### [​](https://docs.anthropic.com/en/docs/build-with-claude/tool-use\#json-output) JSON output Tools do not necessarily need to be client-side functions — you can use tools anytime you want the model to return JSON output that follows a provided schema. For example, you might use a `record_summary` tool with a particular schema. See [tool use examples](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#json-mode) for a full working example. #### [​](https://docs.anthropic.com/en/docs/build-with-claude/tool-use\#chain-of-thought) Chain of thought When using tools, Claude will often show its “chain of thought”, i.e. the step-by-step reasoning it uses to break down the problem and decide which tools to use. The Claude 3 Opus model will do this if `tool_choice` is set to `auto` (this is the default value, see [Forcing tool use](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#forcing-tool-use)), and Sonnet and Haiku can be prompted into doing it. For example, given the prompt “What’s the weather like in San Francisco right now, and what time is it there?”, Claude might respond with: JSON Copy ```JSON { "role": "assistant", "content": [\ {\ "type": "text",\ "text": "To answer this question, I will: 1. Use the get_weather tool to get the current weather in San Francisco. 2. Use the get_time tool to get the current time in the America/Los_Angeles timezone, which covers San Francisco, CA."\ },\ {\ "type": "tool_use",\ "id": "toolu_01A09q90qw90lq917835lq9",\ "name": "get_weather",\ "input": {"location": "San Francisco, CA"}\ }\ ] } ``` This chain of thought gives insight into Claude’s reasoning process and can help you debug unexpected behavior. With the Claude 3 Sonnet model, chain of thought is less common by default, but you can prompt Claude to show its reasoning by adding something like `"Before answering, explain your reasoning step-by-step in tags."` to the user message or system prompt. It’s important to note that while the `` tags are a common convention Claude uses to denote its chain of thought, the exact format (such as what this XML tag is named) may change over time. Your code should treat the chain of thought like any other assistant-generated text, and not rely on the presence or specific formatting of the `` tags. #### [​](https://docs.anthropic.com/en/docs/build-with-claude/tool-use\#disabling-parallel-tool-use) Disabling parallel tool use By default, Claude may use multiple tools to answer a user query. You can disable this behavior by setting `disable_parallel_tool_use=true` in the `tool_choice` field. - When `tool_choice` type is `auto`, this ensures that Claude uses **at most one** tool - When `tool_choice` type is `any` or `tool`, this ensures that Claude uses **exactly one** tool ### [​](https://docs.anthropic.com/en/docs/build-with-claude/tool-use\#handling-tool-use-and-tool-result-content-blocks) Handling tool use and tool result content blocks When Claude decides to use one of the tools you’ve provided, it will return a response with a `stop_reason` of `tool_use` and one or more `tool_use` content blocks in the API response that include: - `id`: A unique identifier for this particular tool use block. This will be used to match up the tool results later. - `name`: The name of the tool being used. - `input`: An object containing the input being passed to the tool, conforming to the tool’s `input_schema`. Example API response with a \`tool\_use\` content block JSON Copy ```JSON { "id": "msg_01Aq9w938a90dw8q", "model": "claude-3-5-sonnet-20241022", "stop_reason": "tool_use", "role": "assistant", "content": [\ {\ "type": "text",\ "text": "I need to use the get_weather, and the user wants SF, which is likely San Francisco, CA."\ },\ {\ "type": "tool_use",\ "id": "toolu_01A09q90qw90lq917835lq9",\ "name": "get_weather",\ "input": {"location": "San Francisco, CA", "unit": "celsius"}\ }\ ] } ``` When you receive a tool use response, you should: 1. Extract the `name`, `id`, and `input` from the `tool_use` block. 2. Run the actual tool in your codebase corresponding to that tool name, passing in the tool `input`. 3. Continue the conversation by sending a new message with the `role` of `user`, and a `content` block containing the `tool_result` type and the following information: - `tool_use_id`: The `id` of the tool use request this is a result for. - `content`: The result of the tool, as a string (e.g. `"content": "15 degrees"`) or list of nested content blocks (e.g. `"content": [{"type": "text", "text": "15 degrees"}]`). These content blocks can use the `text` or `image` types. - `is_error` (optional): Set to `true` if the tool execution resulted in an error. Example of successful tool result JSON Copy ```JSON { "role": "user", "content": [\ {\ "type": "tool_result",\ "tool_use_id": "toolu_01A09q90qw90lq917835lq9",\ "content": "15 degrees"\ }\ ] } ``` Example of tool result with images JSON Copy ```JSON { "role": "user", "content": [\ {\ "type": "tool_result",\ "tool_use_id": "toolu_01A09q90qw90lq917835lq9",\ "content": [\ {"type": "text", "text": "15 degrees"},\ {\ "type": "image",\ "source": {\ "type": "base64",\ "media_type": "image/jpeg",\ "data": "/9j/4AAQSkZJRg...",\ }\ }\ ]\ }\ ] } ``` Example of empty tool result JSON Copy ```JSON { "role": "user", "content": [\ {\ "type": "tool_result",\ "tool_use_id": "toolu_01A09q90qw90lq917835lq9",\ }\ ] } ``` After receiving the tool result, Claude will use that information to continue generating a response to the original user prompt. **Differences from other APIs** Unlike APIs that separate tool use or use special roles like `tool` or `function`, Anthropic’s API integrates tools directly into the `user` and `assistant` message structure. Messages contain arrays of `text`, `image`, `tool_use`, and `tool_result` blocks. `user` messages include client-side content and `tool_result`, while `assistant` messages contain AI-generated content and `tool_use`. ### [​](https://docs.anthropic.com/en/docs/build-with-claude/tool-use\#troubleshooting-errors) Troubleshooting errors There are a few different types of errors that can occur when using tools with Claude: Tool execution error If the tool itself throws an error during execution (e.g. a network error when fetching weather data), you can return the error message in the `content` along with `"is_error": true`: JSON Copy ```JSON { "role": "user", "content": [\ {\ "type": "tool_result",\ "tool_use_id": "toolu_01A09q90qw90lq917835lq9",\ "content": "ConnectionError: the weather service API is not available (HTTP 500)",\ "is_error": true\ }\ ] } ``` Claude will then incorporate this error into its response to the user, e.g. “I’m sorry, I was unable to retrieve the current weather because the weather service API is not available. Please try again later.” Max tokens exceeded If Claude’s response is cut off due to hitting the `max_tokens` limit, and the truncated response contains an incomplete tool use block, you’ll need to retry the request with a higher `max_tokens` value to get the full tool use. Invalid tool name If Claude’s attempted use of a tool is invalid (e.g. missing required parameters), it usually means that the there wasn’t enough information for Claude to use the tool correctly. Your best bet during development is to try the request again with more-detailed `description` values in your tool definitions. However, you can also continue the conversation forward with a `tool_result` that indicates the error, and Claude will try to use the tool again with the missing information filled in: JSON Copy ```JSON { "role": "user", "content": [\ {\ "type": "tool_result",\ "tool_use_id": "toolu_01A09q90qw90lq917835lq9",\ "content": "Error: Missing required 'location' parameter",\ "is_error": true\ }\ ] } ``` If a tool request is invalid or missing parameters, Claude will retry 2-3 times with corrections before apologizing to the user. tags To prevent Claude from reflecting on search quality with tags, add “Do not reflect on the quality of the returned search results in your response” to your prompt. * * * ## [​](https://docs.anthropic.com/en/docs/build-with-claude/tool-use\#tool-use-examples) Tool use examples Here are a few code examples demonstrating various tool use patterns and techniques. For brevity’s sake, the tools are simple tools, and the tool descriptions are shorter than would be ideal to ensure best performance. Single tool example Shell Python Copy ```bash curl https://api.anthropic.com/v1/messages \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" \ --header "content-type: application/json" \ --data \ '{ "model": "claude-3-5-sonnet-20241022", "max_tokens": 1024, "tools": [{\ "name": "get_weather",\ "description": "Get the current weather in a given location",\ "input_schema": {\ "type": "object",\ "properties": {\ "location": {\ "type": "string",\ "description": "The city and state, e.g. San Francisco, CA"\ },\ "unit": {\ "type": "string",\ "enum": ["celsius", "fahrenheit"],\ "description": "The unit of temperature, either \"celsius\" or \"fahrenheit\""\ }\ },\ "required": ["location"]\ }\ }], "messages": [{"role": "user", "content": "What is the weather like in San Francisco?"}] }' ``` Claude will return a response similar to: JSON Copy ```JSON { "id": "msg_01Aq9w938a90dw8q", "model": "claude-3-5-sonnet-20241022", "stop_reason": "tool_use", "role": "assistant", "content": [\ {\ "type": "text",\ "text": "I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA."\ },\ {\ "type": "tool_use",\ "id": "toolu_01A09q90qw90lq917835lq9",\ "name": "get_weather",\ "input": {"location": "San Francisco, CA", "unit": "celsius"}\ }\ ] } ``` You would then need to execute the `get_weather` function with the provided input, and return the result in a new `user` message: Shell Python Copy ```bash curl https://api.anthropic.com/v1/messages \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" \ --header "content-type: application/json" \ --data \ '{ "model": "claude-3-5-sonnet-20241022", "max_tokens": 1024, "tools": [\ {\ "name": "get_weather",\ "description": "Get the current weather in a given location",\ "input_schema": {\ "type": "object",\ "properties": {\ "location": {\ "type": "string",\ "description": "The city and state, e.g. San Francisco, CA"\ },\ "unit": {\ "type": "string",\ "enum": ["celsius", "fahrenheit"],\ "description": "The unit of temperature, either \"celsius\" or \"fahrenheit\""\ }\ },\ "required": ["location"]\ }\ }\ ], "messages": [\ {\ "role": "user",\ "content": "What is the weather like in San Francisco?"\ },\ {\ "role": "assistant",\ "content": [\ {\ "type": "text",\ "text": "I need to use get_weather, and the user wants SF, which is likely San Francisco, CA."\ },\ {\ "type": "tool_use",\ "id": "toolu_01A09q90qw90lq917835lq9",\ "name": "get_weather",\ "input": {\ "location": "San Francisco, CA",\ "unit": "celsius"\ }\ }\ ]\ },\ {\ "role": "user",\ "content": [\ {\ "type": "tool_result",\ "tool_use_id": "toolu_01A09q90qw90lq917835lq9",\ "content": "15 degrees"\ }\ ]\ }\ ] }' ``` This will print Claude’s final response, incorporating the weather data: JSON Copy ```JSON { "id": "msg_01Aq9w938a90dw8q", "model": "claude-3-5-sonnet-20241022", "stop_reason": "stop_sequence", "role": "assistant", "content": [\ {\ "type": "text",\ "text": "The current weather in San Francisco is 15 degrees Celsius (59 degrees Fahrenheit). It's a cool day in the city by the bay!"\ }\ ] } ``` Multiple tool example You can provide Claude with multiple tools to choose from in a single request. Here’s an example with both a `get_weather` and a `get_time` tool, along with a user query that asks for both. Shell Python Copy ```bash curl https://api.anthropic.com/v1/messages \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" \ --header "content-type: application/json" \ --data \ '{ "model": "claude-3-5-sonnet-20241022", "max_tokens": 1024, "tools": [{\ "name": "get_weather",\ "description": "Get the current weather in a given location",\ "input_schema": {\ "type": "object",\ "properties": {\ "location": {\ "type": "string",\ "description": "The city and state, e.g. San Francisco, CA"\ },\ "unit": {\ "type": "string",\ "enum": ["celsius", "fahrenheit"],\ "description": "The unit of temperature, either 'celsius' or 'fahrenheit'"\ }\ },\ "required": ["location"]\ }\ },\ {\ "name": "get_time",\ "description": "Get the current time in a given time zone",\ "input_schema": {\ "type": "object",\ "properties": {\ "timezone": {\ "type": "string",\ "description": "The IANA time zone name, e.g. America/Los_Angeles"\ }\ },\ "required": ["timezone"]\ }\ }], "messages": [{\ "role": "user",\ "content": "What is the weather like right now in New York? Also what time is it there?"\ }] }' ``` In this case, Claude will most likely try to use two separate tools, one at a time — `get_weather` and then `get_time` — in order to fully answer the user’s question. However, it will also occasionally output two `tool_use` blocks at once, particularly if they are not dependent on each other. You would need to execute each tool and return their results in separate `tool_result` blocks within a single `user` message. Missing information If the user’s prompt doesn’t include enough information to fill all the required parameters for a tool, Claude 3 Opus is much more likely to recognize that a parameter is missing and ask for it. Claude 3 Sonnet may ask, especially when prompted to think before outputting a tool request. But it may also do its best to infer a reasonable value. For example, using the `get_weather` tool above, if you ask Claude “What’s the weather?” without specifying a location, Claude, particularly Claude 3 Sonnet, may make a guess about tools inputs: JSON Copy ```JSON { "type": "tool_use", "id": "toolu_01A09q90qw90lq917835lq9", "name": "get_weather", "input": {"location": "New York, NY", "unit": "fahrenheit"} } ``` This behavior is not guaranteed, especially for more ambiguous prompts and for models less intelligent than Claude 3 Opus. If Claude 3 Opus doesn’t have enough context to fill in the required parameters, it is far more likely respond with a clarifying question instead of making a tool call. Sequential tools Some tasks may require calling multiple tools in sequence, using the output of one tool as the input to another. In such a case, Claude will call one tool at a time. If prompted to call the tools all at once, Claude is likely to guess parameters for tools further downstream if they are dependent on tool results for tools further upstream. Here’s an example of using a `get_location` tool to get the user’s location, then passing that location to the `get_weather` tool: Shell Python Copy ```bash curl https://api.anthropic.com/v1/messages \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" \ --header "content-type: application/json" \ --data \ '{ "model": "claude-3-5-sonnet-20241022", "max_tokens": 1024, "tools": [\ {\ "name": "get_location",\ "description": "Get the current user location based on their IP address. This tool has no parameters or arguments.",\ "input_schema": {\ "type": "object",\ "properties": {}\ }\ },\ {\ "name": "get_weather",\ "description": "Get the current weather in a given location",\ "input_schema": {\ "type": "object",\ "properties": {\ "location": {\ "type": "string",\ "description": "The city and state, e.g. San Francisco, CA"\ },\ "unit": {\ "type": "string",\ "enum": ["celsius", "fahrenheit"],\ "description": "The unit of temperature, either 'celsius' or 'fahrenheit'"\ }\ },\ "required": ["location"]\ }\ }\ ], "messages": [{\ "role": "user",\ "content": "What is the weather like where I am?"\ }] }' ``` In this case, Claude would first call the `get_location` tool to get the user’s location. After you return the location in a `tool_result`, Claude would then call `get_weather` with that location to get the final answer. The full conversation might look like: | Role | Content | | --- | --- | | User | What’s the weather like where I am? | | Assistant | To answer this, I first need to determine the user’s location using the get\_location tool. Then I can pass that location to the get\_weather tool to find the current weather there.\[Tool use for get\_location\] | | User | \[Tool result for get\_location with matching id and result of San Francisco, CA\] | | Assistant | \[Tool use for get\_weather with the following input\]{ “location”: “San Francisco, CA”, “unit”: “fahrenheit” } | | User | \[Tool result for get\_weather with matching id and result of “59°F (15°C), mostly cloudy”\] | | Assistant | Based on your current location in San Francisco, CA, the weather right now is 59°F (15°C) and mostly cloudy. It’s a fairly cool and overcast day in the city. You may want to bring a light jacket if you’re heading outside. | This example demonstrates how Claude can chain together multiple tool calls to answer a question that requires gathering data from different sources. The key steps are: 1. Claude first realizes it needs the user’s location to answer the weather question, so it calls the `get_location` tool. 2. The user (i.e. the client code) executes the actual `get_location` function and returns the result “San Francisco, CA” in a `tool_result` block. 3. With the location now known, Claude proceeds to call the `get_weather` tool, passing in “San Francisco, CA” as the `location` parameter (as well as a guessed `unit` parameter, as `unit` is not a required parameter). 4. The user again executes the actual `get_weather` function with the provided arguments and returns the weather data in another `tool_result` block. 5. Finally, Claude incorporates the weather data into a natural language response to the original question. Chain of thought tool use By default, Claude 3 Opus is prompted to think before it answers a tool use query to best determine whether a tool is necessary, which tool to use, and the appropriate parameters. Claude 3 Sonnet and Claude 3 Haiku are prompted to try to use tools as much as possible and are more likely to call an unnecessary tool or infer missing parameters. To prompt Sonnet or Haiku to better assess the user query before making tool calls, the following prompt can be used: Chain of thought prompt `Answer the user's request using relevant tools (if they are available). Before calling a tool, do some analysis within \\ tags. First, think about which of the provided tools is the relevant tool to answer the user's request. Second, go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, close the thinking tag and proceed with the tool call. BUT, if one of the values for a required parameter is missing, DO NOT invoke the function (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters. DO NOT ask for more information on optional parameters if it is not provided. ` JSON mode You can use tools to get Claude produce JSON output that follows a schema, even if you don’t have any intention of running that output through a tool or function. When using tools in this way: - You usually want to provide a **single** tool - You should set `tool_choice` (see [Forcing tool use](https://docs.anthropic.com/en/docs/tool-use#forcing-tool-use)) to instruct the model to explicitly use that tool - Remember that the model will pass the `input` to the tool, so the name of the tool and description should be from the model’s perspective. The following uses a `record_summary` tool to describe an image following a particular format. Shell Python Copy ```bash #!/bin/bash IMAGE_URL="https://upload.wikimedia.org/wikipedia/commons/a/a7/Camponotus_flavomarginatus_ant.jpg" IMAGE_MEDIA_TYPE="image/jpeg" IMAGE_BASE64=$(curl "$IMAGE_URL" | base64) curl https://api.anthropic.com/v1/messages \ --header "content-type: application/json" \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" \ --data \ '{ "model": "claude-3-5-sonnet-latest", "max_tokens": 1024, "tools": [{\ "name": "record_summary",\ "description": "Record summary of an image using well-structured JSON.",\ "input_schema": {\ "type": "object",\ "properties": {\ "key_colors": {\ "type": "array",\ "items": {\ "type": "object",\ "properties": {\ "r": { "type": "number", "description": "red value [0.0, 1.0]" },\ "g": { "type": "number", "description": "green value [0.0, 1.0]" },\ "b": { "type": "number", "description": "blue value [0.0, 1.0]" },\ "name": { "type": "string", "description": "Human-readable color name in snake_case, e.g. \"olive_green\" or \"turquoise\"" }\ },\ "required": [ "r", "g", "b", "name" ]\ },\ "description": "Key colors in the image. Limit to less then four."\ },\ "description": {\ "type": "string",\ "description": "Image description. One to two sentences max."\ },\ "estimated_year": {\ "type": "integer",\ "description": "Estimated year that the images was taken, if is it a photo. Only set this if the image appears to be non-fictional. Rough estimates are okay!"\ }\ },\ "required": [ "key_colors", "description" ]\ }\ }], "tool_choice": {"type": "tool", "name": "record_summary"}, "messages": [\ {"role": "user", "content": [\ {"type": "image", "source": {\ "type": "base64",\ "media_type": "'$IMAGE_MEDIA_TYPE'",\ "data": "'$IMAGE_BASE64'"\ }},\ {"type": "text", "text": "Describe this image."}\ ]}\ ] }' ``` * * * ## [​](https://docs.anthropic.com/en/docs/build-with-claude/tool-use\#pricing) Pricing Tool use requests are priced the same as any other Claude API request, based on the total number of input tokens sent to the model (including in the `tools` parameter) and the number of output tokens generated.” The additional tokens from tool use come from: - The `tools` parameter in API requests (tool names, descriptions, and schemas) - `tool_use` content blocks in API requests and responses - `tool_result` content blocks in API requests When you use `tools`, we also automatically include a special system prompt for the model which enables tool use. The number of tool use tokens required for each model are listed below (excluding the additional tokens listed above): | Model | Tool choice | Tool use system prompt token count | | --- | --- | --- | | Claude 3.5 Sonnet (Oct) | `auto`
* * *
`any`, `tool` | 346 tokens
* * *
313 tokens | | Claude 3 Opus | `auto`
* * *
`any`, `tool` | 530 tokens
* * *
281 tokens | | Claude 3 Sonnet | `auto`
* * *
`any`, `tool` | 159 tokens
* * *
235 tokens | | Claude 3 Haiku | `auto`
* * *
`any`, `tool` | 264 tokens
* * *
340 tokens | | Claude 3.5 Sonnet (June) | `auto`
* * *
`any`, `tool` | 294 tokens
* * *
261 tokens | These token counts are added to your normal input and output tokens to calculate the total cost of a request. Refer to our [models overview table](https://docs.anthropic.com/en/docs/models-overview#model-comparison) for current per-model prices. When you send a tool use prompt, just like any other API request, the response will output both input and output token counts as part of the reported `usage` metrics. * * * ## [​](https://docs.anthropic.com/en/docs/build-with-claude/tool-use\#next-steps) Next Steps Explore our repository of ready-to-implement tool use code examples in our cookbooks: [**Calculator Tool** \\ \\ Learn how to integrate a simple calculator tool with Claude for precise numerical computations.](https://github.com/anthropics/anthropic-cookbook/blob/main/tool_use/calculator_tool.ipynb) [**Customer Service Agent** \\ \\ Build a responsive customer service bot that leverages client-side tools to\\ enhance support.](https://github.com/anthropics/anthropic-cookbook/blob/main/tool_use/customer_service_agent.ipynb) [**JSON Extractor** \\ \\ See how Claude and tool use can extract structured data from unstructured text.](https://github.com/anthropics/anthropic-cookbook/blob/main/tool_use/extracting_structured_json.ipynb) Was this page helpful? YesNo [Vision](https://docs.anthropic.com/en/docs/build-with-claude/vision) [Model Context Protocol (MCP)](https://docs.anthropic.com/en/docs/build-with-claude/mcp) On this page - [How tool use works](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#how-tool-use-works) - [How to implement tool use](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#how-to-implement-tool-use) - [Choosing a model](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#choosing-a-model) - [Specifying tools](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#specifying-tools) - [Tool use system prompt](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-system-prompt) - [Best practices for tool definitions](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#best-practices-for-tool-definitions) - [Controlling Claude’s output](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#controlling-claudes-output) - [Forcing tool use](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#forcing-tool-use) - [JSON output](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#json-output) - [Chain of thought](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#chain-of-thought) - [Disabling parallel tool use](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#disabling-parallel-tool-use) - [Handling tool use and tool result content blocks](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#handling-tool-use-and-tool-result-content-blocks) - [Troubleshooting errors](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#troubleshooting-errors) - [Tool use examples](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples) - [Pricing](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#pricing) - [Next Steps](https://docs.anthropic.com/en/docs/build-with-claude/tool-use#next-steps) ![](https://docs.anthropic.com/en/docs/build-with-claude/tool-use) ================================================ FILE: ai_docs/python_genai.md ================================================ # Google Gen AI SDK [Permalink: Google Gen AI SDK](https://github.com/googleapis/python-genai#google-gen-ai-sdk) [![PyPI version](https://camo.githubusercontent.com/af4dae966695dbde629839adb60210ed763579c6f73cf6159ed8aa64e68fd35b/68747470733a2f2f696d672e736869656c64732e696f2f707970692f762f676f6f676c652d67656e61692e737667)](https://pypi.org/project/google-genai/) * * * **Documentation:** [https://googleapis.github.io/python-genai/](https://googleapis.github.io/python-genai/) * * * Google Gen AI Python SDK provides an interface for developers to integrate Google's generative models into their Python applications. It supports the [Gemini Developer API](https://ai.google.dev/gemini-api/docs) and [Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/overview) APIs. ## Installation [Permalink: Installation](https://github.com/googleapis/python-genai#installation) ``` pip install google-genai ``` ## Imports [Permalink: Imports](https://github.com/googleapis/python-genai#imports) ``` from google import genai from google.genai import types ``` ## Create a client [Permalink: Create a client](https://github.com/googleapis/python-genai#create-a-client) Please run one of the following code blocks to create a client for different services ( [Gemini Developer API](https://ai.google.dev/gemini-api/docs) or [Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/overview)). ``` # Only run this block for Gemini Developer API client = genai.Client(api_key='GEMINI_API_KEY') ``` ``` # Only run this block for Vertex AI API client = genai.Client( vertexai=True, project='your-project-id', location='us-central1' ) ``` **(Optional) Using environment variables:** You can create a client by configuring the necessary environment variables. Configuration setup instructions depends on whether you're using the Gemini API on Vertex AI or the ML Dev Gemini API. **ML Dev Gemini API:** Set `GOOGLE_API_KEY` as shown below: ``` export GOOGLE_API_KEY='your-api-key' ``` **Vertex AI API:** Set `GOOGLE_GENAI_USE_VERTEXAI`, `GOOGLE_CLOUD_PROJECT` and `GOOGLE_CLOUD_LOCATION`, as shown below: ``` export GOOGLE_GENAI_USE_VERTEXAI=false export GOOGLE_CLOUD_PROJECT='your-project-id' export GOOGLE_CLOUD_LOCATION='us-central1' ``` ``` client = genai.Client() ``` ### API Selection [Permalink: API Selection](https://github.com/googleapis/python-genai#api-selection) To set the API version use `http_options`. For example, to set the API version to `v1` for Vertex AI: ``` client = genai.Client( vertexai=True, project='your-project-id', location='us-central1', http_options={'api_version': 'v1'} ) ``` To set the API version to `v1alpha` for the Gemini API: ``` client = genai.Client(api_key='GEMINI_API_KEY', http_options={'api_version': 'v1alpha'}) ``` ## Types [Permalink: Types](https://github.com/googleapis/python-genai#types) Parameter types can be specified as either dictionaries( `TypedDict`) or [Pydantic Models](https://pydantic.readthedocs.io/en/stable/model.html). Pydantic model types are available in the `types` module. ## Models [Permalink: Models](https://github.com/googleapis/python-genai#models) The `client.models` modules exposes model inferencing and model getters. ### Generate Content [Permalink: Generate Content](https://github.com/googleapis/python-genai#generate-content) #### with text content [Permalink: with text content](https://github.com/googleapis/python-genai#with-text-content) ``` response = client.models.generate_content( model='gemini-2.0-flash-001', contents='why is the sky blue?' ) print(response.text) ``` #### with uploaded file (Gemini API only) [Permalink: with uploaded file (Gemini API only)](https://github.com/googleapis/python-genai#with-uploaded-file-gemini-api-only) download the file in console. ``` !wget -q https://storage.googleapis.com/generativeai-downloads/data/a11.txt ``` python code. ``` file = client.files.upload(file='a11.txt') response = client.models.generate_content( model='gemini-2.0-flash-001', contents=['Could you summarize this file?', file] ) print(response.text) ``` #### How to structure `contents` [Permalink: How to structure contents](https://github.com/googleapis/python-genai#how-to-structure-contents) There are several ways to structure the `contents` in your request. Provide a single string as shown in the text example above: ``` contents='Can you recommend some things to do in Boston and New York in the winter?' ``` Provide a single `Content` instance with multiple `Part` instances: ``` contents=types.Content(parts=[\ types.Part.from_text(text='Can you recommend some things to do in Boston in the winter?'),\ types.Part.from_text(text='Can you recommend some things to do in New York in the winter?')\ ], role='user') ``` When sending more than one input type, provide a list with multiple `Content` instances: ``` contents=[\ 'What is this a picture of?',\ types.Part.from_uri(\ file_uri='gs://generativeai-downloads/images/scones.jpg',\ mime_type='image/jpeg',\ ),\ ], ``` ### System Instructions and Other Configs [Permalink: System Instructions and Other Configs](https://github.com/googleapis/python-genai#system-instructions-and-other-configs) ``` response = client.models.generate_content( model='gemini-2.0-flash-001', contents='high', config=types.GenerateContentConfig( system_instruction='I say high, you say low', temperature=0.3, ), ) print(response.text) ``` ### Typed Config [Permalink: Typed Config](https://github.com/googleapis/python-genai#typed-config) All API methods support Pydantic types for parameters as well as dictionaries. You can get the type from `google.genai.types`. ``` response = client.models.generate_content( model='gemini-2.0-flash-001', contents=types.Part.from_text(text='Why is the sky blue?'), config=types.GenerateContentConfig( temperature=0, top_p=0.95, top_k=20, candidate_count=1, seed=5, max_output_tokens=100, stop_sequences=['STOP!'], presence_penalty=0.0, frequency_penalty=0.0, ), ) print(response.text) ``` ### List Base Models [Permalink: List Base Models](https://github.com/googleapis/python-genai#list-base-models) To retrieve tuned models, see [list tuned models](https://github.com/googleapis/python-genai#list-tuned-models). ``` for model in client.models.list(): print(model) ``` ``` pager = client.models.list(config={'page_size': 10}) print(pager.page_size) print(pager[0]) pager.next_page() print(pager[0]) ``` #### Async [Permalink: Async](https://github.com/googleapis/python-genai#async) ``` async for job in await client.aio.models.list(): print(job) ``` ``` async_pager = await client.aio.models.list(config={'page_size': 10}) print(async_pager.page_size) print(async_pager[0]) await async_pager.next_page() print(async_pager[0]) ``` ### Safety Settings [Permalink: Safety Settings](https://github.com/googleapis/python-genai#safety-settings) ``` response = client.models.generate_content( model='gemini-2.0-flash-001', contents='Say something bad.', config=types.GenerateContentConfig( safety_settings=[\ types.SafetySetting(\ category='HARM_CATEGORY_HATE_SPEECH',\ threshold='BLOCK_ONLY_HIGH',\ )\ ] ), ) print(response.text) ``` ### Function Calling [Permalink: Function Calling](https://github.com/googleapis/python-genai#function-calling) #### Automatic Python function Support [Permalink: Automatic Python function Support](https://github.com/googleapis/python-genai#automatic-python-function-support) You can pass a Python function directly and it will be automatically called and responded. ``` def get_current_weather(location: str) -> str: """Returns the current weather. Args: location: The city and state, e.g. San Francisco, CA """ return 'sunny' response = client.models.generate_content( model='gemini-2.0-flash-001', contents='What is the weather like in Boston?', config=types.GenerateContentConfig(tools=[get_current_weather]), ) print(response.text) ``` #### Manually declare and invoke a function for function calling [Permalink: Manually declare and invoke a function for function calling](https://github.com/googleapis/python-genai#manually-declare-and-invoke-a-function-for-function-calling) If you don't want to use the automatic function support, you can manually declare the function and invoke it. The following example shows how to declare a function and pass it as a tool. Then you will receive a function call part in the response. ``` function = types.FunctionDeclaration( name='get_current_weather', description='Get the current weather in a given location', parameters=types.Schema( type='OBJECT', properties={ 'location': types.Schema( type='STRING', description='The city and state, e.g. San Francisco, CA', ), }, required=['location'], ), ) tool = types.Tool(function_declarations=[function]) response = client.models.generate_content( model='gemini-2.0-flash-001', contents='What is the weather like in Boston?', config=types.GenerateContentConfig(tools=[tool]), ) print(response.function_calls[0]) ``` After you receive the function call part from the model, you can invoke the function and get the function response. And then you can pass the function response to the model. The following example shows how to do it for a simple function invocation. ``` user_prompt_content = types.Content( role='user', parts=[types.Part.from_text(text='What is the weather like in Boston?')], ) function_call_part = response.function_calls[0] function_call_content = response.candidates[0].content try: function_result = get_current_weather( **function_call_part.function_call.args ) function_response = {'result': function_result} except ( Exception ) as e: # instead of raising the exception, you can let the model handle it function_response = {'error': str(e)} function_response_part = types.Part.from_function_response( name=function_call_part.name, response=function_response, ) function_response_content = types.Content( role='tool', parts=[function_response_part] ) response = client.models.generate_content( model='gemini-2.0-flash-001', contents=[\ user_prompt_content,\ function_call_content,\ function_response_content,\ ], config=types.GenerateContentConfig( tools=[tool], ), ) print(response.text) ``` #### Function calling with `ANY` tools config mode [Permalink: Function calling with ANY tools config mode](https://github.com/googleapis/python-genai#function-calling-with-any-tools-config-mode) If you configure function calling mode to be `ANY`, then the model will always return function call parts. If you also pass a python function as a tool, by default the SDK will perform automatic function calling until the remote calls exceed the maximum remote call for automatic function calling (default to 10 times). If you'd like to disable automatic function calling in `ANY` mode: ``` def get_current_weather(location: str) -> str: """Returns the current weather. Args: location: The city and state, e.g. San Francisco, CA """ return "sunny" response = client.models.generate_content( model="gemini-2.0-flash-001", contents="What is the weather like in Boston?", config=types.GenerateContentConfig( tools=[get_current_weather], automatic_function_calling=types.AutomaticFunctionCallingConfig( disable=True ), tool_config=types.ToolConfig( function_calling_config=types.FunctionCallingConfig(mode='ANY') ), ), ) ``` If you'd like to set `x` number of automatic function call turns, you can configure the maximum remote calls to be `x + 1`. Assuming you prefer `1` turn for automatic function calling. ``` def get_current_weather(location: str) -> str: """Returns the current weather. Args: location: The city and state, e.g. San Francisco, CA """ return "sunny" response = client.models.generate_content( model="gemini-2.0-flash-001", contents="What is the weather like in Boston?", config=types.GenerateContentConfig( tools=[get_current_weather], automatic_function_calling=types.AutomaticFunctionCallingConfig( maximum_remote_calls=2 ), tool_config=types.ToolConfig( function_calling_config=types.FunctionCallingConfig(mode='ANY') ), ), ) ``` ### JSON Response Schema [Permalink: JSON Response Schema](https://github.com/googleapis/python-genai#json-response-schema) #### Pydantic Model Schema support [Permalink: Pydantic Model Schema support](https://github.com/googleapis/python-genai#pydantic-model-schema-support) Schemas can be provided as Pydantic Models. ``` from pydantic import BaseModel class CountryInfo(BaseModel): name: str population: int capital: str continent: str gdp: int official_language: str total_area_sq_mi: int response = client.models.generate_content( model='gemini-2.0-flash-001', contents='Give me information for the United States.', config=types.GenerateContentConfig( response_mime_type='application/json', response_schema=CountryInfo, ), ) print(response.text) ``` ``` response = client.models.generate_content( model='gemini-2.0-flash-001', contents='Give me information for the United States.', config=types.GenerateContentConfig( response_mime_type='application/json', response_schema={ 'required': [\ 'name',\ 'population',\ 'capital',\ 'continent',\ 'gdp',\ 'official_language',\ 'total_area_sq_mi',\ ], 'properties': { 'name': {'type': 'STRING'}, 'population': {'type': 'INTEGER'}, 'capital': {'type': 'STRING'}, 'continent': {'type': 'STRING'}, 'gdp': {'type': 'INTEGER'}, 'official_language': {'type': 'STRING'}, 'total_area_sq_mi': {'type': 'INTEGER'}, }, 'type': 'OBJECT', }, ), ) print(response.text) ``` ### Enum Response Schema [Permalink: Enum Response Schema](https://github.com/googleapis/python-genai#enum-response-schema) #### Text Response [Permalink: Text Response](https://github.com/googleapis/python-genai#text-response) You can set response\_mime\_type to 'text/x.enum' to return one of those enum values as the response. ``` class InstrumentEnum(Enum): PERCUSSION = 'Percussion' STRING = 'String' WOODWIND = 'Woodwind' BRASS = 'Brass' KEYBOARD = 'Keyboard' response = client.models.generate_content( model='gemini-2.0-flash-001', contents='What instrument plays multiple notes at once?', config={ 'response_mime_type': 'text/x.enum', 'response_schema': InstrumentEnum, }, ) print(response.text) ``` #### JSON Response [Permalink: JSON Response](https://github.com/googleapis/python-genai#json-response) You can also set response\_mime\_type to 'application/json', the response will be identical but in quotes. ``` from enum import Enum class InstrumentEnum(Enum): PERCUSSION = 'Percussion' STRING = 'String' WOODWIND = 'Woodwind' BRASS = 'Brass' KEYBOARD = 'Keyboard' response = client.models.generate_content( model='gemini-2.0-flash-001', contents='What instrument plays multiple notes at once?', config={ 'response_mime_type': 'application/json', 'response_schema': InstrumentEnum, }, ) print(response.text) ``` ### Streaming [Permalink: Streaming](https://github.com/googleapis/python-genai#streaming) #### Streaming for text content [Permalink: Streaming for text content](https://github.com/googleapis/python-genai#streaming-for-text-content) ``` for chunk in client.models.generate_content_stream( model='gemini-2.0-flash-001', contents='Tell me a story in 300 words.' ): print(chunk.text, end='') ``` #### Streaming for image content [Permalink: Streaming for image content](https://github.com/googleapis/python-genai#streaming-for-image-content) If your image is stored in [Google Cloud Storage](https://cloud.google.com/storage), you can use the `from_uri` class method to create a `Part` object. ``` for chunk in client.models.generate_content_stream( model='gemini-2.0-flash-001', contents=[\ 'What is this image about?',\ types.Part.from_uri(\ file_uri='gs://generativeai-downloads/images/scones.jpg',\ mime_type='image/jpeg',\ ),\ ], ): print(chunk.text, end='') ``` If your image is stored in your local file system, you can read it in as bytes data and use the `from_bytes` class method to create a `Part` object. ``` YOUR_IMAGE_PATH = 'your_image_path' YOUR_IMAGE_MIME_TYPE = 'your_image_mime_type' with open(YOUR_IMAGE_PATH, 'rb') as f: image_bytes = f.read() for chunk in client.models.generate_content_stream( model='gemini-2.0-flash-001', contents=[\ 'What is this image about?',\ types.Part.from_bytes(data=image_bytes, mime_type=YOUR_IMAGE_MIME_TYPE),\ ], ): print(chunk.text, end='') ``` ### Async [Permalink: Async](https://github.com/googleapis/python-genai#async-1) `client.aio` exposes all the analogous [`async` methods](https://docs.python.org/3/library/asyncio.html) that are available on `client` For example, `client.aio.models.generate_content` is the `async` version of `client.models.generate_content` ``` response = await client.aio.models.generate_content( model='gemini-2.0-flash-001', contents='Tell me a story in 300 words.' ) print(response.text) ``` ### Streaming [Permalink: Streaming](https://github.com/googleapis/python-genai#streaming-1) ``` async for chunk in await client.aio.models.generate_content_stream( model='gemini-2.0-flash-001', contents='Tell me a story in 300 words.' ): print(chunk.text, end='') ``` ### Count Tokens and Compute Tokens [Permalink: Count Tokens and Compute Tokens](https://github.com/googleapis/python-genai#count-tokens-and-compute-tokens) ``` response = client.models.count_tokens( model='gemini-2.0-flash-001', contents='why is the sky blue?', ) print(response) ``` #### Compute Tokens [Permalink: Compute Tokens](https://github.com/googleapis/python-genai#compute-tokens) Compute tokens is only supported in Vertex AI. ``` response = client.models.compute_tokens( model='gemini-2.0-flash-001', contents='why is the sky blue?', ) print(response) ``` ##### Async [Permalink: Async](https://github.com/googleapis/python-genai#async-2) ``` response = await client.aio.models.count_tokens( model='gemini-2.0-flash-001', contents='why is the sky blue?', ) print(response) ``` ### Embed Content [Permalink: Embed Content](https://github.com/googleapis/python-genai#embed-content) ``` response = client.models.embed_content( model='text-embedding-004', contents='why is the sky blue?', ) print(response) ``` ``` # multiple contents with config response = client.models.embed_content( model='text-embedding-004', contents=['why is the sky blue?', 'What is your age?'], config=types.EmbedContentConfig(output_dimensionality=10), ) print(response) ``` ### Imagen [Permalink: Imagen](https://github.com/googleapis/python-genai#imagen) #### Generate Images [Permalink: Generate Images](https://github.com/googleapis/python-genai#generate-images) Support for generate images in Gemini Developer API is behind an allowlist ``` # Generate Image response1 = client.models.generate_images( model='imagen-3.0-generate-002', prompt='An umbrella in the foreground, and a rainy night sky in the background', config=types.GenerateImagesConfig( negative_prompt='human', number_of_images=1, include_rai_reason=True, output_mime_type='image/jpeg', ), ) response1.generated_images[0].image.show() ``` #### Upscale Image [Permalink: Upscale Image](https://github.com/googleapis/python-genai#upscale-image) Upscale image is only supported in Vertex AI. ``` # Upscale the generated image from above response2 = client.models.upscale_image( model='imagen-3.0-generate-001', image=response1.generated_images[0].image, upscale_factor='x2', config=types.UpscaleImageConfig( include_rai_reason=True, output_mime_type='image/jpeg', ), ) response2.generated_images[0].image.show() ``` #### Edit Image [Permalink: Edit Image](https://github.com/googleapis/python-genai#edit-image) Edit image uses a separate model from generate and upscale. Edit image is only supported in Vertex AI. ``` # Edit the generated image from above from google.genai.types import RawReferenceImage, MaskReferenceImage raw_ref_image = RawReferenceImage( reference_id=1, reference_image=response1.generated_images[0].image, ) # Model computes a mask of the background mask_ref_image = MaskReferenceImage( reference_id=2, config=types.MaskReferenceConfig( mask_mode='MASK_MODE_BACKGROUND', mask_dilation=0, ), ) response3 = client.models.edit_image( model='imagen-3.0-capability-001', prompt='Sunlight and clear sky', reference_images=[raw_ref_image, mask_ref_image], config=types.EditImageConfig( edit_mode='EDIT_MODE_INPAINT_INSERTION', number_of_images=1, negative_prompt='human', include_rai_reason=True, output_mime_type='image/jpeg', ), ) response3.generated_images[0].image.show() ``` ## Chats [Permalink: Chats](https://github.com/googleapis/python-genai#chats) Create a chat session to start a multi-turn conversations with the model. ### Send Message [Permalink: Send Message](https://github.com/googleapis/python-genai#send-message) ``` chat = client.chats.create(model='gemini-2.0-flash-001') response = chat.send_message('tell me a story') print(response.text) ``` ### Streaming [Permalink: Streaming](https://github.com/googleapis/python-genai#streaming-2) ``` chat = client.chats.create(model='gemini-2.0-flash-001') for chunk in chat.send_message_stream('tell me a story'): print(chunk.text) ``` ### Async [Permalink: Async](https://github.com/googleapis/python-genai#async-3) ``` chat = client.aio.chats.create(model='gemini-2.0-flash-001') response = await chat.send_message('tell me a story') print(response.text) ``` ### Async Streaming [Permalink: Async Streaming](https://github.com/googleapis/python-genai#async-streaming) ``` chat = client.aio.chats.create(model='gemini-2.0-flash-001') async for chunk in await chat.send_message_stream('tell me a story'): print(chunk.text) ``` ## Files [Permalink: Files](https://github.com/googleapis/python-genai#files) Files are only supported in Gemini Developer API. ``` !gsutil cp gs://cloud-samples-data/generative-ai/pdf/2312.11805v3.pdf . !gsutil cp gs://cloud-samples-data/generative-ai/pdf/2403.05530.pdf . ``` ### Upload [Permalink: Upload](https://github.com/googleapis/python-genai#upload) ``` file1 = client.files.upload(file='2312.11805v3.pdf') file2 = client.files.upload(file='2403.05530.pdf') print(file1) print(file2) ``` ### Get [Permalink: Get](https://github.com/googleapis/python-genai#get) ``` file1 = client.files.upload(file='2312.11805v3.pdf') file_info = client.files.get(name=file1.name) ``` ### Delete [Permalink: Delete](https://github.com/googleapis/python-genai#delete) ``` file3 = client.files.upload(file='2312.11805v3.pdf') client.files.delete(name=file3.name) ``` ## Caches [Permalink: Caches](https://github.com/googleapis/python-genai#caches) `client.caches` contains the control plane APIs for cached content ### Create [Permalink: Create](https://github.com/googleapis/python-genai#create) ``` if client.vertexai: file_uris = [\ 'gs://cloud-samples-data/generative-ai/pdf/2312.11805v3.pdf',\ 'gs://cloud-samples-data/generative-ai/pdf/2403.05530.pdf',\ ] else: file_uris = [file1.uri, file2.uri] cached_content = client.caches.create( model='gemini-1.5-pro-002', config=types.CreateCachedContentConfig( contents=[\ types.Content(\ role='user',\ parts=[\ types.Part.from_uri(\ file_uri=file_uris[0], mime_type='application/pdf'\ ),\ types.Part.from_uri(\ file_uri=file_uris[1],\ mime_type='application/pdf',\ ),\ ],\ )\ ], system_instruction='What is the sum of the two pdfs?', display_name='test cache', ttl='3600s', ), ) ``` ### Get [Permalink: Get](https://github.com/googleapis/python-genai#get-1) ``` cached_content = client.caches.get(name=cached_content.name) ``` ### Generate Content with Caches [Permalink: Generate Content with Caches](https://github.com/googleapis/python-genai#generate-content-with-caches) ``` response = client.models.generate_content( model='gemini-1.5-pro-002', contents='Summarize the pdfs', config=types.GenerateContentConfig( cached_content=cached_content.name, ), ) print(response.text) ``` ## Tunings [Permalink: Tunings](https://github.com/googleapis/python-genai#tunings) `client.tunings` contains tuning job APIs and supports supervised fine tuning through `tune`. ### Tune [Permalink: Tune](https://github.com/googleapis/python-genai#tune) - Vertex AI supports tuning from GCS source - Gemini Developer API supports tuning from inline examples ``` if client.vertexai: model = 'gemini-1.5-pro-002' training_dataset = types.TuningDataset( gcs_uri='gs://cloud-samples-data/ai-platform/generative_ai/gemini-1_5/text/sft_train_data.jsonl', ) else: model = 'models/gemini-1.0-pro-001' training_dataset = types.TuningDataset( examples=[\ types.TuningExample(\ text_input=f'Input text {i}',\ output=f'Output text {i}',\ )\ for i in range(5)\ ], ) ``` ``` tuning_job = client.tunings.tune( base_model=model, training_dataset=training_dataset, config=types.CreateTuningJobConfig( epoch_count=1, tuned_model_display_name='test_dataset_examples model' ), ) print(tuning_job) ``` ### Get Tuning Job [Permalink: Get Tuning Job](https://github.com/googleapis/python-genai#get-tuning-job) ``` tuning_job = client.tunings.get(name=tuning_job.name) print(tuning_job) ``` ``` import time running_states = set( [\ 'JOB_STATE_PENDING',\ 'JOB_STATE_RUNNING',\ ] ) while tuning_job.state in running_states: print(tuning_job.state) tuning_job = client.tunings.get(name=tuning_job.name) time.sleep(10) ``` #### Use Tuned Model [Permalink: Use Tuned Model](https://github.com/googleapis/python-genai#use-tuned-model) ``` response = client.models.generate_content( model=tuning_job.tuned_model.endpoint, contents='why is the sky blue?', ) print(response.text) ``` ### Get Tuned Model [Permalink: Get Tuned Model](https://github.com/googleapis/python-genai#get-tuned-model) ``` tuned_model = client.models.get(model=tuning_job.tuned_model.model) print(tuned_model) ``` ### List Tuned Models [Permalink: List Tuned Models](https://github.com/googleapis/python-genai#list-tuned-models) To retrieve base models, see [list base models](https://github.com/googleapis/python-genai#list-base-models). ``` for model in client.models.list(config={'page_size': 10, 'query_base': False}): print(model) ``` ``` pager = client.models.list(config={'page_size': 10, 'query_base': False}) print(pager.page_size) print(pager[0]) pager.next_page() print(pager[0]) ``` #### Async [Permalink: Async](https://github.com/googleapis/python-genai#async-4) ``` async for job in await client.aio.models.list(config={'page_size': 10, 'query_base': False}): print(job) ``` ``` async_pager = await client.aio.models.list(config={'page_size': 10, 'query_base': False}) print(async_pager.page_size) print(async_pager[0]) await async_pager.next_page() print(async_pager[0]) ``` ### Update Tuned Model [Permalink: Update Tuned Model](https://github.com/googleapis/python-genai#update-tuned-model) ``` model = pager[0] model = client.models.update( model=model.name, config=types.UpdateModelConfig( display_name='my tuned model', description='my tuned model description' ), ) print(model) ``` ### List Tuning Jobs [Permalink: List Tuning Jobs](https://github.com/googleapis/python-genai#list-tuning-jobs) ``` for job in client.tunings.list(config={'page_size': 10}): print(job) ``` ``` pager = client.tunings.list(config={'page_size': 10}) print(pager.page_size) print(pager[0]) pager.next_page() print(pager[0]) ``` #### Async [Permalink: Async](https://github.com/googleapis/python-genai#async-5) ``` async for job in await client.aio.tunings.list(config={'page_size': 10}): print(job) ``` ``` async_pager = await client.aio.tunings.list(config={'page_size': 10}) print(async_pager.page_size) print(async_pager[0]) await async_pager.next_page() print(async_pager[0]) ``` ## Batch Prediction [Permalink: Batch Prediction](https://github.com/googleapis/python-genai#batch-prediction) Only supported in Vertex AI. ### Create [Permalink: Create](https://github.com/googleapis/python-genai#create-1) ``` # Specify model and source file only, destination and job display name will be auto-populated job = client.batches.create( model='gemini-1.5-flash-002', src='bq://my-project.my-dataset.my-table', ) job ``` ``` # Get a job by name job = client.batches.get(name=job.name) job.state ``` ``` completed_states = set( [\ 'JOB_STATE_SUCCEEDED',\ 'JOB_STATE_FAILED',\ 'JOB_STATE_CANCELLED',\ 'JOB_STATE_PAUSED',\ ] ) while job.state not in completed_states: print(job.state) job = client.batches.get(name=job.name) time.sleep(30) job ``` ### List [Permalink: List](https://github.com/googleapis/python-genai#list) ``` for job in client.batches.list(config=types.ListBatchJobsConfig(page_size=10)): print(job) ``` ``` pager = client.batches.list(config=types.ListBatchJobsConfig(page_size=10)) print(pager.page_size) print(pager[0]) pager.next_page() print(pager[0]) ``` #### Async [Permalink: Async](https://github.com/googleapis/python-genai#async-6) ``` async for job in await client.aio.batches.list( config=types.ListBatchJobsConfig(page_size=10) ): print(job) ``` ``` async_pager = await client.aio.batches.list( config=types.ListBatchJobsConfig(page_size=10) ) print(async_pager.page_size) print(async_pager[0]) await async_pager.next_page() print(async_pager[0]) ``` ### Delete [Permalink: Delete](https://github.com/googleapis/python-genai#delete-1) ``` # Delete the job resource delete_job = client.batches.delete(name=job.name) delete_job ``` ## About Google Gen AI Python SDK provides an interface for developers to integrate Google's generative models into their Python applications. [googleapis.github.io/python-genai/](https://googleapis.github.io/python-genai/ "https://googleapis.github.io/python-genai/") ### Resources [Readme](https://github.com/googleapis/python-genai#readme-ov-file) ### License [Apache-2.0 license](https://github.com/googleapis/python-genai#Apache-2.0-1-ov-file) ### Code of conduct [Code of conduct](https://github.com/googleapis/python-genai#coc-ov-file) ### Security policy [Security policy](https://github.com/googleapis/python-genai#security-ov-file) [Activity](https://github.com/googleapis/python-genai/activity) [Custom properties](https://github.com/googleapis/python-genai/custom-properties) ### Stars [**927**\\ stars](https://github.com/googleapis/python-genai/stargazers) ### Watchers [**80**\\ watching](https://github.com/googleapis/python-genai/watchers) ### Forks [**159**\\ forks](https://github.com/googleapis/python-genai/forks) [Report repository](https://github.com/contact/report-content?content_url=https%3A%2F%2Fgithub.com%2Fgoogleapis%2Fpython-genai&report=googleapis+%28user%29) ## [Releases\ 9](https://github.com/googleapis/python-genai/releases) [v1.1.0\\ Latest\\ \\ Feb 10, 2025](https://github.com/googleapis/python-genai/releases/tag/v1.1.0) [\+ 8 releases](https://github.com/googleapis/python-genai/releases) ## [Packages\ 0](https://github.com/orgs/googleapis/packages?repo_name=python-genai) No packages published ## [Used by 1.2k](https://github.com/googleapis/python-genai/network/dependents) [- ![@haoyuliao](https://avatars.githubusercontent.com/u/23026269?s=64&v=4)\\ - ![@timroty](https://avatars.githubusercontent.com/u/42622148?s=64&v=4)\\ - ![@Lobooooooo14](https://avatars.githubusercontent.com/u/88998991?s=64&v=4)\\ - ![@derrismaqebe](https://avatars.githubusercontent.com/u/193747737?s=64&v=4)\\ - ![@GoUpvote](https://avatars.githubusercontent.com/u/118300643?s=64&v=4)\\ - ![@Frisyk](https://avatars.githubusercontent.com/u/112816171?s=64&v=4)\\ - ![@ParhamNajarzadeh](https://avatars.githubusercontent.com/u/116252212?s=64&v=4)\\ - ![@kuangsith](https://avatars.githubusercontent.com/u/108988177?s=64&v=4)\\ \\ \+ 1,215](https://github.com/googleapis/python-genai/network/dependents) ## [Contributors\ 23](https://github.com/googleapis/python-genai/graphs/contributors) - [![@sasha-gitg](https://avatars.githubusercontent.com/u/44654632?s=64&v=4)](https://github.com/sasha-gitg) - [![@happy-qiao](https://avatars.githubusercontent.com/u/159568575?s=64&v=4)](https://github.com/happy-qiao) - [![@sararob](https://avatars.githubusercontent.com/u/3814898?s=64&v=4)](https://github.com/sararob) - [![@hkt74](https://avatars.githubusercontent.com/u/4653660?s=64&v=4)](https://github.com/hkt74) - [![@google-genai-bot](https://avatars.githubusercontent.com/u/194307901?s=64&v=4)](https://github.com/google-genai-bot) - [![@yyyu-google](https://avatars.githubusercontent.com/u/150068659?s=64&v=4)](https://github.com/yyyu-google) - [![@amirh](https://avatars.githubusercontent.com/u/1024117?s=64&v=4)](https://github.com/amirh) - [![@Ark-kun](https://avatars.githubusercontent.com/u/1829149?s=64&v=4)](https://github.com/Ark-kun) - [![@yinghsienwu](https://avatars.githubusercontent.com/u/14824050?s=64&v=4)](https://github.com/yinghsienwu) - [![@matthew29tang](https://avatars.githubusercontent.com/u/22719762?s=64&v=4)](https://github.com/matthew29tang) - [![@release-please[bot]](https://avatars.githubusercontent.com/in/40688?s=64&v=4)](https://github.com/apps/release-please) - [![@MarkDaoust](https://avatars.githubusercontent.com/u/1414837?s=64&v=4)](https://github.com/MarkDaoust) - [![@Annhiluc](https://avatars.githubusercontent.com/u/10099501?s=64&v=4)](https://github.com/Annhiluc) [\+ 9 contributors](https://github.com/googleapis/python-genai/graphs/contributors) ## Languages - [Python100.0%](https://github.com/googleapis/python-genai/search?l=python) You can’t perform that action at this time. ================================================ FILE: ai_docs/python_openai.md ================================================ [Skip to content](https://github.com/openai/openai-python#start-of-content) You signed in with another tab or window. [Reload](https://github.com/openai/openai-python) to refresh your session.You signed out in another tab or window. [Reload](https://github.com/openai/openai-python) to refresh your session.You switched accounts on another tab or window. [Reload](https://github.com/openai/openai-python) to refresh your session.Dismiss alert [openai](https://github.com/openai)/ **[openai-python](https://github.com/openai/openai-python)** Public - [Notifications](https://github.com/login?return_to=%2Fopenai%2Fopenai-python) You must be signed in to change notification settings - [Fork\\ 3.6k](https://github.com/login?return_to=%2Fopenai%2Fopenai-python) - [Star\\ 24.6k](https://github.com/login?return_to=%2Fopenai%2Fopenai-python) The official Python library for the OpenAI API [pypi.org/project/openai/](https://pypi.org/project/openai/ "https://pypi.org/project/openai/") ### License [Apache-2.0 license](https://github.com/openai/openai-python/blob/main/LICENSE) [24.6k\\ stars](https://github.com/openai/openai-python/stargazers) [3.6k\\ forks](https://github.com/openai/openai-python/forks) [Branches](https://github.com/openai/openai-python/branches) [Tags](https://github.com/openai/openai-python/tags) [Activity](https://github.com/openai/openai-python/activity) [Star](https://github.com/login?return_to=%2Fopenai%2Fopenai-python) [Notifications](https://github.com/login?return_to=%2Fopenai%2Fopenai-python) You must be signed in to change notification settings # openai/openai-python main [**12** Branches](https://github.com/openai/openai-python/branches) [**243** Tags](https://github.com/openai/openai-python/tags) [Go to Branches page](https://github.com/openai/openai-python/branches)[Go to Tags page](https://github.com/openai/openai-python/tags) Go to file Code ## Folders and files | Name | Name | Last commit message | Last commit date | | --- | --- | --- | --- | | ## Latest commit
[![stainless-app[bot]](https://avatars.githubusercontent.com/in/378072?v=4&size=40)](https://github.com/apps/stainless-app)[stainless-app\[bot\]](https://github.com/openai/openai-python/commits?author=stainless-app%5Bbot%5D)
[release: 1.63.0](https://github.com/openai/openai-python/commit/720ae54414f392202289578c9cc3b84cccc7432c)
Feb 13, 2025
[720ae54](https://github.com/openai/openai-python/commit/720ae54414f392202289578c9cc3b84cccc7432c) · Feb 13, 2025
## History
[816 Commits](https://github.com/openai/openai-python/commits/main/) | | [.devcontainer](https://github.com/openai/openai-python/tree/main/.devcontainer ".devcontainer") | [.devcontainer](https://github.com/openai/openai-python/tree/main/.devcontainer ".devcontainer") | [chore(ci): update rye to v0.35.0 (](https://github.com/openai/openai-python/commit/94fc49d8b198b4b9fe98bf22883ed82b060e865b "chore(ci): update rye to v0.35.0 (#1523)") [#1523](https://github.com/openai/openai-python/pull/1523) [)](https://github.com/openai/openai-python/commit/94fc49d8b198b4b9fe98bf22883ed82b060e865b "chore(ci): update rye to v0.35.0 (#1523)") | Jul 3, 2024 | | [.github](https://github.com/openai/openai-python/tree/main/.github ".github") | [.github](https://github.com/openai/openai-python/tree/main/.github ".github") | [chore(internal): minor formatting changes (](https://github.com/openai/openai-python/commit/27d0e67b1d121ccc5b48c95e1f0bc3f6e93e9bd3 "chore(internal): minor formatting changes (#2050)") [#2050](https://github.com/openai/openai-python/pull/2050) [)](https://github.com/openai/openai-python/commit/27d0e67b1d121ccc5b48c95e1f0bc3f6e93e9bd3 "chore(internal): minor formatting changes (#2050)") | Jan 24, 2025 | | [.inline-snapshot/external](https://github.com/openai/openai-python/tree/main/.inline-snapshot/external "This path skips through empty directories") | [.inline-snapshot/external](https://github.com/openai/openai-python/tree/main/.inline-snapshot/external "This path skips through empty directories") | [chore(internal): update test snapshots (](https://github.com/openai/openai-python/commit/9feadd8274809fff9ff1e36a0c90d45566ed46e2 "chore(internal): update test snapshots (#1749)") [#1749](https://github.com/openai/openai-python/pull/1749) [)](https://github.com/openai/openai-python/commit/9feadd8274809fff9ff1e36a0c90d45566ed46e2 "chore(internal): update test snapshots (#1749)") | Sep 26, 2024 | | [bin](https://github.com/openai/openai-python/tree/main/bin "bin") | [bin](https://github.com/openai/openai-python/tree/main/bin "bin") | [fix: temporarily patch upstream version to fix broken release flow (](https://github.com/openai/openai-python/commit/8061d18dd8bb1f9f17a46ac0d90edb2592a132a0 "fix: temporarily patch upstream version to fix broken release flow (#1500)") [#…](https://github.com/openai/openai-python/pull/1500) | Jun 25, 2024 | | [examples](https://github.com/openai/openai-python/tree/main/examples "examples") | [examples](https://github.com/openai/openai-python/tree/main/examples "examples") | [docs(examples/azure): add async snippet (](https://github.com/openai/openai-python/commit/abc5459c7504eec25a67b35104e2e09e7d8f232c "docs(examples/azure): add async snippet (#1787)") [#1787](https://github.com/openai/openai-python/pull/1787) [)](https://github.com/openai/openai-python/commit/abc5459c7504eec25a67b35104e2e09e7d8f232c "docs(examples/azure): add async snippet (#1787)") | Jan 24, 2025 | | [scripts](https://github.com/openai/openai-python/tree/main/scripts "scripts") | [scripts](https://github.com/openai/openai-python/tree/main/scripts "scripts") | [chore(internal): bummp ruff dependency (](https://github.com/openai/openai-python/commit/6afde0dc8512a16ff2eca781fee0395cab254f8c "chore(internal): bummp ruff dependency (#2080)") [#2080](https://github.com/openai/openai-python/pull/2080) [)](https://github.com/openai/openai-python/commit/6afde0dc8512a16ff2eca781fee0395cab254f8c "chore(internal): bummp ruff dependency (#2080)") | Feb 5, 2025 | | [src/openai](https://github.com/openai/openai-python/tree/main/src/openai "This path skips through empty directories") | [src/openai](https://github.com/openai/openai-python/tree/main/src/openai "This path skips through empty directories") | [release: 1.63.0](https://github.com/openai/openai-python/commit/720ae54414f392202289578c9cc3b84cccc7432c "release: 1.63.0") | Feb 13, 2025 | | [tests](https://github.com/openai/openai-python/tree/main/tests "tests") | [tests](https://github.com/openai/openai-python/tree/main/tests "tests") | [feat(api): add support for storing chat completions (](https://github.com/openai/openai-python/commit/300f58bbbde749e023dd1cf39de8f5339780a33d "feat(api): add support for storing chat completions (#2117)") [#2117](https://github.com/openai/openai-python/pull/2117) [)](https://github.com/openai/openai-python/commit/300f58bbbde749e023dd1cf39de8f5339780a33d "feat(api): add support for storing chat completions (#2117)") | Feb 13, 2025 | | [.gitignore](https://github.com/openai/openai-python/blob/main/.gitignore ".gitignore") | [.gitignore](https://github.com/openai/openai-python/blob/main/.gitignore ".gitignore") | [chore: gitignore test server logs (](https://github.com/openai/openai-python/commit/24347efc6d94faddb10b773b04c2b5afa38b2ea6 "chore: gitignore test server logs (#1509)") [#1509](https://github.com/openai/openai-python/pull/1509) [)](https://github.com/openai/openai-python/commit/24347efc6d94faddb10b773b04c2b5afa38b2ea6 "chore: gitignore test server logs (#1509)") | Jul 2, 2024 | | [.python-version](https://github.com/openai/openai-python/blob/main/.python-version ".python-version") | [.python-version](https://github.com/openai/openai-python/blob/main/.python-version ".python-version") | [V1 (](https://github.com/openai/openai-python/commit/08b8179a6b3e46ca8eb117f819cc6563ae74e27d "V1 (#677) * cleanup * v1.0.0-beta.1 * docs: add basic manual azure example * docs: use chat completions instead of completions for demo example * test: rename `API_BASE_URL` to `TEST_API_BASE_URL` * feat(client): handle retry-after header with a date format * feat(api): remove `content_filter` stop_reason and update documentation * refactor(cli): rename internal types for improved auto complete * feat(client): add forwards-compatible pydantic methods * feat(api): move `n_epochs` under `hyperparameters` * feat(client): add support for passing in a httpx client * chore: update README * feat(cli): use http/2 if h2 is available * chore(docs): remove trailing spaces * feat(client): add logging setup * chore(internal): minor updates * v1.0.0-beta.2 * docs: use chat completions instead of completions for demo example * chore: add case insensitive get header function * fix(client): correctly handle errors during streaming * fix(streaming): add additional overload for ambiguous stream param * chore(internal): enable lint rule * chore(internal): cleanup some redundant code * fix(client): accept io.IOBase instances in file params * docs: improve error message for invalid file param type * 1.0.0-beta.3 * chore(internal): migrate from Poetry to Rye * feat(cli): add `tools fine_tunes.prepare_data` * feat(client): support passing httpx.URL instances to base_url * chore(internal): fix some latent type errors * feat(api): add embeddings encoding_format * feat: use numpy for faster embeddings decoding * chore(internal): bump pyright * chore(internal): bump deps * feat(client): improve file upload types * feat(client): adjust retry behavior to be exponential backoff * ci: add lint workflow * docs: improve to dictionary example * ci(lint): run ruff too * chore(internal): require explicit overrides * feat(client): support accessing raw response objects * test(qs): add an additional test case for array brackets * feat(client): add dedicated Azure client * feat(package): add classifiers * docs(readme): add Azure guide * 1.0.0-rc1 * docs: small cleanup * feat(github): include a devcontainer setup * chore: improve type names * feat(client): allow binary returns * feat(client): support passing BaseModels to request params at runtime * fix(binaries): don't synchronously block in astream_to_file * 1.0.0-rc2 * chore(internal): remove unused int/float conversion * docs(readme): improve example snippets * fix: prevent TypeError in Python 3.8 (ABC is not subscriptable) * 1.0.0-rc3 * docs: update streaming example * docs(readme): update opening * v1.0.0 --------- Co-authored-by: Robert Craigie Co-authored-by: Stainless Bot <107565488+stainless-bot@users.noreply.github.com> Co-authored-by: Stainless Bot Co-authored-by: Alex Rattray ") [#677](https://github.com/openai/openai-python/pull/677) [)](https://github.com/openai/openai-python/commit/08b8179a6b3e46ca8eb117f819cc6563ae74e27d "V1 (#677) * cleanup * v1.0.0-beta.1 * docs: add basic manual azure example * docs: use chat completions instead of completions for demo example * test: rename `API_BASE_URL` to `TEST_API_BASE_URL` * feat(client): handle retry-after header with a date format * feat(api): remove `content_filter` stop_reason and update documentation * refactor(cli): rename internal types for improved auto complete * feat(client): add forwards-compatible pydantic methods * feat(api): move `n_epochs` under `hyperparameters` * feat(client): add support for passing in a httpx client * chore: update README * feat(cli): use http/2 if h2 is available * chore(docs): remove trailing spaces * feat(client): add logging setup * chore(internal): minor updates * v1.0.0-beta.2 * docs: use chat completions instead of completions for demo example * chore: add case insensitive get header function * fix(client): correctly handle errors during streaming * fix(streaming): add additional overload for ambiguous stream param * chore(internal): enable lint rule * chore(internal): cleanup some redundant code * fix(client): accept io.IOBase instances in file params * docs: improve error message for invalid file param type * 1.0.0-beta.3 * chore(internal): migrate from Poetry to Rye * feat(cli): add `tools fine_tunes.prepare_data` * feat(client): support passing httpx.URL instances to base_url * chore(internal): fix some latent type errors * feat(api): add embeddings encoding_format * feat: use numpy for faster embeddings decoding * chore(internal): bump pyright * chore(internal): bump deps * feat(client): improve file upload types * feat(client): adjust retry behavior to be exponential backoff * ci: add lint workflow * docs: improve to dictionary example * ci(lint): run ruff too * chore(internal): require explicit overrides * feat(client): support accessing raw response objects * test(qs): add an additional test case for array brackets * feat(client): add dedicated Azure client * feat(package): add classifiers * docs(readme): add Azure guide * 1.0.0-rc1 * docs: small cleanup * feat(github): include a devcontainer setup * chore: improve type names * feat(client): allow binary returns * feat(client): support passing BaseModels to request params at runtime * fix(binaries): don't synchronously block in astream_to_file * 1.0.0-rc2 * chore(internal): remove unused int/float conversion * docs(readme): improve example snippets * fix: prevent TypeError in Python 3.8 (ABC is not subscriptable) * 1.0.0-rc3 * docs: update streaming example * docs(readme): update opening * v1.0.0 --------- Co-authored-by: Robert Craigie Co-authored-by: Stainless Bot <107565488+stainless-bot@users.noreply.github.com> Co-authored-by: Stainless Bot Co-authored-by: Alex Rattray ") | Nov 6, 2023 | | [.release-please-manifest.json](https://github.com/openai/openai-python/blob/main/.release-please-manifest.json ".release-please-manifest.json") | [.release-please-manifest.json](https://github.com/openai/openai-python/blob/main/.release-please-manifest.json ".release-please-manifest.json") | [release: 1.63.0](https://github.com/openai/openai-python/commit/720ae54414f392202289578c9cc3b84cccc7432c "release: 1.63.0") | Feb 13, 2025 | | [.stats.yml](https://github.com/openai/openai-python/blob/main/.stats.yml ".stats.yml") | [.stats.yml](https://github.com/openai/openai-python/blob/main/.stats.yml ".stats.yml") | [feat(api): add support for storing chat completions (](https://github.com/openai/openai-python/commit/300f58bbbde749e023dd1cf39de8f5339780a33d "feat(api): add support for storing chat completions (#2117)") [#2117](https://github.com/openai/openai-python/pull/2117) [)](https://github.com/openai/openai-python/commit/300f58bbbde749e023dd1cf39de8f5339780a33d "feat(api): add support for storing chat completions (#2117)") | Feb 13, 2025 | | [Brewfile](https://github.com/openai/openai-python/blob/main/Brewfile "Brewfile") | [Brewfile](https://github.com/openai/openai-python/blob/main/Brewfile "Brewfile") | [feat(api): delete messages (](https://github.com/openai/openai-python/commit/d2738d4259aa1c58e206ec23e388855fa218d3f9 "feat(api): delete messages (#1388)") [#1388](https://github.com/openai/openai-python/pull/1388) [)](https://github.com/openai/openai-python/commit/d2738d4259aa1c58e206ec23e388855fa218d3f9 "feat(api): delete messages (#1388)") | Apr 30, 2024 | | [CHANGELOG.md](https://github.com/openai/openai-python/blob/main/CHANGELOG.md "CHANGELOG.md") | [CHANGELOG.md](https://github.com/openai/openai-python/blob/main/CHANGELOG.md "CHANGELOG.md") | [release: 1.63.0](https://github.com/openai/openai-python/commit/720ae54414f392202289578c9cc3b84cccc7432c "release: 1.63.0") | Feb 13, 2025 | | [CONTRIBUTING.md](https://github.com/openai/openai-python/blob/main/CONTRIBUTING.md "CONTRIBUTING.md") | [CONTRIBUTING.md](https://github.com/openai/openai-python/blob/main/CONTRIBUTING.md "CONTRIBUTING.md") | [docs: fix typo in fenced code block language (](https://github.com/openai/openai-python/commit/50de514b910aced32104833acd892bebfb2cf123 "docs: fix typo in fenced code block language (#1769)") [#1769](https://github.com/openai/openai-python/pull/1769) [)](https://github.com/openai/openai-python/commit/50de514b910aced32104833acd892bebfb2cf123 "docs: fix typo in fenced code block language (#1769)") | Oct 7, 2024 | | [LICENSE](https://github.com/openai/openai-python/blob/main/LICENSE "LICENSE") | [LICENSE](https://github.com/openai/openai-python/blob/main/LICENSE "LICENSE") | [chore: bump license year (](https://github.com/openai/openai-python/commit/99861632e9bdb1a480d92913d621bded574bf797 "chore: bump license year (#1981)") [#1981](https://github.com/openai/openai-python/pull/1981) [)](https://github.com/openai/openai-python/commit/99861632e9bdb1a480d92913d621bded574bf797 "chore: bump license year (#1981)") | Jan 2, 2025 | | [README.md](https://github.com/openai/openai-python/blob/main/README.md "README.md") | [README.md](https://github.com/openai/openai-python/blob/main/README.md "README.md") | [docs(readme): current section links (](https://github.com/openai/openai-python/commit/90e3d39655548c935002dee7ef6f617c846c123c "docs(readme): current section links (#2055) chore(helpers): section links") [#2055](https://github.com/openai/openai-python/pull/2055) [)](https://github.com/openai/openai-python/commit/90e3d39655548c935002dee7ef6f617c846c123c "docs(readme): current section links (#2055) chore(helpers): section links") | Jan 31, 2025 | | [SECURITY.md](https://github.com/openai/openai-python/blob/main/SECURITY.md "SECURITY.md") | [SECURITY.md](https://github.com/openai/openai-python/blob/main/SECURITY.md "SECURITY.md") | [chore(docs): add SECURITY.md (](https://github.com/openai/openai-python/commit/1aeaf02f662e6e925180ddfcaa26508408b2f2a4 "chore(docs): add SECURITY.md (#1408)") [#1408](https://github.com/openai/openai-python/pull/1408) [)](https://github.com/openai/openai-python/commit/1aeaf02f662e6e925180ddfcaa26508408b2f2a4 "chore(docs): add SECURITY.md (#1408)") | May 10, 2024 | | [api.md](https://github.com/openai/openai-python/blob/main/api.md "api.md") | [api.md](https://github.com/openai/openai-python/blob/main/api.md "api.md") | [feat(api): add support for storing chat completions (](https://github.com/openai/openai-python/commit/300f58bbbde749e023dd1cf39de8f5339780a33d "feat(api): add support for storing chat completions (#2117)") [#2117](https://github.com/openai/openai-python/pull/2117) [)](https://github.com/openai/openai-python/commit/300f58bbbde749e023dd1cf39de8f5339780a33d "feat(api): add support for storing chat completions (#2117)") | Feb 13, 2025 | | [helpers.md](https://github.com/openai/openai-python/blob/main/helpers.md "helpers.md") | [helpers.md](https://github.com/openai/openai-python/blob/main/helpers.md "helpers.md") | [docs(readme): current section links (](https://github.com/openai/openai-python/commit/90e3d39655548c935002dee7ef6f617c846c123c "docs(readme): current section links (#2055) chore(helpers): section links") [#2055](https://github.com/openai/openai-python/pull/2055) [)](https://github.com/openai/openai-python/commit/90e3d39655548c935002dee7ef6f617c846c123c "docs(readme): current section links (#2055) chore(helpers): section links") | Jan 31, 2025 | | [mypy.ini](https://github.com/openai/openai-python/blob/main/mypy.ini "mypy.ini") | [mypy.ini](https://github.com/openai/openai-python/blob/main/mypy.ini "mypy.ini") | [chore(internal): update deps (](https://github.com/openai/openai-python/commit/83f11490fa291f9814f3dae6a65b1f62d0177675 "chore(internal): update deps (#2015)") [#2015](https://github.com/openai/openai-python/pull/2015) [)](https://github.com/openai/openai-python/commit/83f11490fa291f9814f3dae6a65b1f62d0177675 "chore(internal): update deps (#2015)") | Jan 17, 2025 | | [noxfile.py](https://github.com/openai/openai-python/blob/main/noxfile.py "noxfile.py") | [noxfile.py](https://github.com/openai/openai-python/blob/main/noxfile.py "noxfile.py") | [V1 (](https://github.com/openai/openai-python/commit/08b8179a6b3e46ca8eb117f819cc6563ae74e27d "V1 (#677) * cleanup * v1.0.0-beta.1 * docs: add basic manual azure example * docs: use chat completions instead of completions for demo example * test: rename `API_BASE_URL` to `TEST_API_BASE_URL` * feat(client): handle retry-after header with a date format * feat(api): remove `content_filter` stop_reason and update documentation * refactor(cli): rename internal types for improved auto complete * feat(client): add forwards-compatible pydantic methods * feat(api): move `n_epochs` under `hyperparameters` * feat(client): add support for passing in a httpx client * chore: update README * feat(cli): use http/2 if h2 is available * chore(docs): remove trailing spaces * feat(client): add logging setup * chore(internal): minor updates * v1.0.0-beta.2 * docs: use chat completions instead of completions for demo example * chore: add case insensitive get header function * fix(client): correctly handle errors during streaming * fix(streaming): add additional overload for ambiguous stream param * chore(internal): enable lint rule * chore(internal): cleanup some redundant code * fix(client): accept io.IOBase instances in file params * docs: improve error message for invalid file param type * 1.0.0-beta.3 * chore(internal): migrate from Poetry to Rye * feat(cli): add `tools fine_tunes.prepare_data` * feat(client): support passing httpx.URL instances to base_url * chore(internal): fix some latent type errors * feat(api): add embeddings encoding_format * feat: use numpy for faster embeddings decoding * chore(internal): bump pyright * chore(internal): bump deps * feat(client): improve file upload types * feat(client): adjust retry behavior to be exponential backoff * ci: add lint workflow * docs: improve to dictionary example * ci(lint): run ruff too * chore(internal): require explicit overrides * feat(client): support accessing raw response objects * test(qs): add an additional test case for array brackets * feat(client): add dedicated Azure client * feat(package): add classifiers * docs(readme): add Azure guide * 1.0.0-rc1 * docs: small cleanup * feat(github): include a devcontainer setup * chore: improve type names * feat(client): allow binary returns * feat(client): support passing BaseModels to request params at runtime * fix(binaries): don't synchronously block in astream_to_file * 1.0.0-rc2 * chore(internal): remove unused int/float conversion * docs(readme): improve example snippets * fix: prevent TypeError in Python 3.8 (ABC is not subscriptable) * 1.0.0-rc3 * docs: update streaming example * docs(readme): update opening * v1.0.0 --------- Co-authored-by: Robert Craigie Co-authored-by: Stainless Bot <107565488+stainless-bot@users.noreply.github.com> Co-authored-by: Stainless Bot Co-authored-by: Alex Rattray ") [#677](https://github.com/openai/openai-python/pull/677) [)](https://github.com/openai/openai-python/commit/08b8179a6b3e46ca8eb117f819cc6563ae74e27d "V1 (#677) * cleanup * v1.0.0-beta.1 * docs: add basic manual azure example * docs: use chat completions instead of completions for demo example * test: rename `API_BASE_URL` to `TEST_API_BASE_URL` * feat(client): handle retry-after header with a date format * feat(api): remove `content_filter` stop_reason and update documentation * refactor(cli): rename internal types for improved auto complete * feat(client): add forwards-compatible pydantic methods * feat(api): move `n_epochs` under `hyperparameters` * feat(client): add support for passing in a httpx client * chore: update README * feat(cli): use http/2 if h2 is available * chore(docs): remove trailing spaces * feat(client): add logging setup * chore(internal): minor updates * v1.0.0-beta.2 * docs: use chat completions instead of completions for demo example * chore: add case insensitive get header function * fix(client): correctly handle errors during streaming * fix(streaming): add additional overload for ambiguous stream param * chore(internal): enable lint rule * chore(internal): cleanup some redundant code * fix(client): accept io.IOBase instances in file params * docs: improve error message for invalid file param type * 1.0.0-beta.3 * chore(internal): migrate from Poetry to Rye * feat(cli): add `tools fine_tunes.prepare_data` * feat(client): support passing httpx.URL instances to base_url * chore(internal): fix some latent type errors * feat(api): add embeddings encoding_format * feat: use numpy for faster embeddings decoding * chore(internal): bump pyright * chore(internal): bump deps * feat(client): improve file upload types * feat(client): adjust retry behavior to be exponential backoff * ci: add lint workflow * docs: improve to dictionary example * ci(lint): run ruff too * chore(internal): require explicit overrides * feat(client): support accessing raw response objects * test(qs): add an additional test case for array brackets * feat(client): add dedicated Azure client * feat(package): add classifiers * docs(readme): add Azure guide * 1.0.0-rc1 * docs: small cleanup * feat(github): include a devcontainer setup * chore: improve type names * feat(client): allow binary returns * feat(client): support passing BaseModels to request params at runtime * fix(binaries): don't synchronously block in astream_to_file * 1.0.0-rc2 * chore(internal): remove unused int/float conversion * docs(readme): improve example snippets * fix: prevent TypeError in Python 3.8 (ABC is not subscriptable) * 1.0.0-rc3 * docs: update streaming example * docs(readme): update opening * v1.0.0 --------- Co-authored-by: Robert Craigie Co-authored-by: Stainless Bot <107565488+stainless-bot@users.noreply.github.com> Co-authored-by: Stainless Bot Co-authored-by: Alex Rattray ") | Nov 6, 2023 | | [pyproject.toml](https://github.com/openai/openai-python/blob/main/pyproject.toml "pyproject.toml") | [pyproject.toml](https://github.com/openai/openai-python/blob/main/pyproject.toml "pyproject.toml") | [release: 1.63.0](https://github.com/openai/openai-python/commit/720ae54414f392202289578c9cc3b84cccc7432c "release: 1.63.0") | Feb 13, 2025 | | [release-please-config.json](https://github.com/openai/openai-python/blob/main/release-please-config.json "release-please-config.json") | [release-please-config.json](https://github.com/openai/openai-python/blob/main/release-please-config.json "release-please-config.json") | [chore(internal): support pre-release versioning (](https://github.com/openai/openai-python/commit/336cf03092376c0b54cc2cfbd78167c1aac01af7 "chore(internal): support pre-release versioning (#1113)") [#1113](https://github.com/openai/openai-python/pull/1113) [)](https://github.com/openai/openai-python/commit/336cf03092376c0b54cc2cfbd78167c1aac01af7 "chore(internal): support pre-release versioning (#1113)") | Feb 2, 2024 | | [requirements-dev.lock](https://github.com/openai/openai-python/blob/main/requirements-dev.lock "requirements-dev.lock") | [requirements-dev.lock](https://github.com/openai/openai-python/blob/main/requirements-dev.lock "requirements-dev.lock") | [chore(internal): bummp ruff dependency (](https://github.com/openai/openai-python/commit/6afde0dc8512a16ff2eca781fee0395cab254f8c "chore(internal): bummp ruff dependency (#2080)") [#2080](https://github.com/openai/openai-python/pull/2080) [)](https://github.com/openai/openai-python/commit/6afde0dc8512a16ff2eca781fee0395cab254f8c "chore(internal): bummp ruff dependency (#2080)") | Feb 5, 2025 | | [requirements.lock](https://github.com/openai/openai-python/blob/main/requirements.lock "requirements.lock") | [requirements.lock](https://github.com/openai/openai-python/blob/main/requirements.lock "requirements.lock") | [chore(internal): update websockets dep (](https://github.com/openai/openai-python/commit/1e5e19976a02f4f3423cf7e32ad5fa020c857b82 "chore(internal): update websockets dep (#2036)") [#2036](https://github.com/openai/openai-python/pull/2036) [)](https://github.com/openai/openai-python/commit/1e5e19976a02f4f3423cf7e32ad5fa020c857b82 "chore(internal): update websockets dep (#2036)") | Jan 20, 2025 | | View all files | ## Repository files navigation # OpenAI Python API library [Permalink: OpenAI Python API library](https://github.com/openai/openai-python#openai-python-api-library) [![PyPI version](https://camo.githubusercontent.com/b2f318dcc71bb9b8e6021c196a2cce69a3a64e721ddc19c4904d42f84d0219ac/68747470733a2f2f696d672e736869656c64732e696f2f707970692f762f6f70656e61692e737667)](https://pypi.org/project/openai/) The OpenAI Python library provides convenient access to the OpenAI REST API from any Python 3.8+ application. The library includes type definitions for all request params and response fields, and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). It is generated from our [OpenAPI specification](https://github.com/openai/openai-openapi) with [Stainless](https://stainlessapi.com/). ## Documentation [Permalink: Documentation](https://github.com/openai/openai-python#documentation) The REST API documentation can be found on [platform.openai.com](https://platform.openai.com/docs). The full API of this library can be found in [api.md](https://github.com/openai/openai-python/blob/main/api.md). ## Installation [Permalink: Installation](https://github.com/openai/openai-python#installation) Important The SDK was rewritten in v1, which was released November 6th 2023. See the [v1 migration guide](https://github.com/openai/openai-python/discussions/742), which includes scripts to automatically update your code. ``` # install from PyPI pip install openai ``` ## Usage [Permalink: Usage](https://github.com/openai/openai-python#usage) The full API of this library can be found in [api.md](https://github.com/openai/openai-python/blob/main/api.md). ``` import os from openai import OpenAI client = OpenAI( api_key=os.environ.get("OPENAI_API_KEY"), # This is the default and can be omitted ) chat_completion = client.chat.completions.create( messages=[\ {\ "role": "user",\ "content": "Say this is a test",\ }\ ], model="gpt-4o", ) ``` While you can provide an `api_key` keyword argument, we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) to add `OPENAI_API_KEY="My API Key"` to your `.env` file so that your API Key is not stored in source control. ### Vision [Permalink: Vision](https://github.com/openai/openai-python#vision) With a hosted image: ``` response = client.chat.completions.create( model="gpt-4o-mini", messages=[\ {\ "role": "user",\ "content": [\ {"type": "text", "text": prompt},\ {\ "type": "image_url",\ "image_url": {"url": f"{img_url}"},\ },\ ],\ }\ ], ) ``` With the image as a base64 encoded string: ``` response = client.chat.completions.create( model="gpt-4o-mini", messages=[\ {\ "role": "user",\ "content": [\ {"type": "text", "text": prompt},\ {\ "type": "image_url",\ "image_url": {"url": f"data:{img_type};base64,{img_b64_str}"},\ },\ ],\ }\ ], ) ``` ### Polling Helpers [Permalink: Polling Helpers](https://github.com/openai/openai-python#polling-helpers) When interacting with the API some actions such as starting a Run and adding files to vector stores are asynchronous and take time to complete. The SDK includes helper functions which will poll the status until it reaches a terminal state and then return the resulting object. If an API method results in an action that could benefit from polling there will be a corresponding version of the method ending in '\_and\_poll'. For instance to create a Run and poll until it reaches a terminal state you can run: ``` run = client.beta.threads.runs.create_and_poll( thread_id=thread.id, assistant_id=assistant.id, ) ``` More information on the lifecycle of a Run can be found in the [Run Lifecycle Documentation](https://platform.openai.com/docs/assistants/how-it-works/run-lifecycle) ### Bulk Upload Helpers [Permalink: Bulk Upload Helpers](https://github.com/openai/openai-python#bulk-upload-helpers) When creating and interacting with vector stores, you can use polling helpers to monitor the status of operations. For convenience, we also provide a bulk upload helper to allow you to simultaneously upload several files at once. ``` sample_files = [Path("sample-paper.pdf"), ...] batch = await client.vector_stores.file_batches.upload_and_poll( store.id, files=sample_files, ) ``` ### Streaming Helpers [Permalink: Streaming Helpers](https://github.com/openai/openai-python#streaming-helpers) The SDK also includes helpers to process streams and handle incoming events. ``` with client.beta.threads.runs.stream( thread_id=thread.id, assistant_id=assistant.id, instructions="Please address the user as Jane Doe. The user has a premium account.", ) as stream: for event in stream: # Print the text from text delta events if event.type == "thread.message.delta" and event.data.delta.content: print(event.data.delta.content[0].text) ``` More information on streaming helpers can be found in the dedicated documentation: [helpers.md](https://github.com/openai/openai-python/blob/main/helpers.md) ## Async usage [Permalink: Async usage](https://github.com/openai/openai-python#async-usage) Simply import `AsyncOpenAI` instead of `OpenAI` and use `await` with each API call: ``` import os import asyncio from openai import AsyncOpenAI client = AsyncOpenAI( api_key=os.environ.get("OPENAI_API_KEY"), # This is the default and can be omitted ) async def main() -> None: chat_completion = await client.chat.completions.create( messages=[\ {\ "role": "user",\ "content": "Say this is a test",\ }\ ], model="gpt-4o", ) asyncio.run(main()) ``` Functionality between the synchronous and asynchronous clients is otherwise identical. ## Streaming responses [Permalink: Streaming responses](https://github.com/openai/openai-python#streaming-responses) We provide support for streaming responses using Server Side Events (SSE). ``` from openai import OpenAI client = OpenAI() stream = client.chat.completions.create( messages=[\ {\ "role": "user",\ "content": "Say this is a test",\ }\ ], model="gpt-4o", stream=True, ) for chunk in stream: print(chunk.choices[0].delta.content or "", end="") ``` The async client uses the exact same interface. ``` import asyncio from openai import AsyncOpenAI client = AsyncOpenAI() async def main(): stream = await client.chat.completions.create( model="gpt-4", messages=[{"role": "user", "content": "Say this is a test"}], stream=True, ) async for chunk in stream: print(chunk.choices[0].delta.content or "", end="") asyncio.run(main()) ``` ## Module-level client [Permalink: Module-level client](https://github.com/openai/openai-python#module-level-client) Important We highly recommend instantiating client instances instead of relying on the global client. We also expose a global client instance that is accessible in a similar fashion to versions prior to v1. ``` import openai # optional; defaults to `os.environ['OPENAI_API_KEY']` openai.api_key = '...' # all client options can be configured just like the `OpenAI` instantiation counterpart openai.base_url = "https://..." openai.default_headers = {"x-foo": "true"} completion = openai.chat.completions.create( model="gpt-4o", messages=[\ {\ "role": "user",\ "content": "How do I output all files in a directory using Python?",\ },\ ], ) print(completion.choices[0].message.content) ``` The API is the exact same as the standard client instance-based API. This is intended to be used within REPLs or notebooks for faster iteration, **not** in application code. We recommend that you always instantiate a client (e.g., with `client = OpenAI()`) in application code because: - It can be difficult to reason about where client options are configured - It's not possible to change certain client options without potentially causing race conditions - It's harder to mock for testing purposes - It's not possible to control cleanup of network connections ## Realtime API beta [Permalink: Realtime API beta](https://github.com/openai/openai-python#realtime-api-beta) The Realtime API enables you to build low-latency, multi-modal conversational experiences. It currently supports text and audio as both input and output, as well as [function calling](https://platform.openai.com/docs/guides/function-calling) through a WebSocket connection. Under the hood the SDK uses the [`websockets`](https://websockets.readthedocs.io/en/stable/) library to manage connections. The Realtime API works through a combination of client-sent events and server-sent events. Clients can send events to do things like update session configuration or send text and audio inputs. Server events confirm when audio responses have completed, or when a text response from the model has been received. A full event reference can be found [here](https://platform.openai.com/docs/api-reference/realtime-client-events) and a guide can be found [here](https://platform.openai.com/docs/guides/realtime). Basic text based example: ``` import asyncio from openai import AsyncOpenAI async def main(): client = AsyncOpenAI() async with client.beta.realtime.connect(model="gpt-4o-realtime-preview") as connection: await connection.session.update(session={'modalities': ['text']}) await connection.conversation.item.create( item={ "type": "message", "role": "user", "content": [{"type": "input_text", "text": "Say hello!"}], } ) await connection.response.create() async for event in connection: if event.type == 'response.text.delta': print(event.delta, flush=True, end="") elif event.type == 'response.text.done': print() elif event.type == "response.done": break asyncio.run(main()) ``` However the real magic of the Realtime API is handling audio inputs / outputs, see this example [TUI script](https://github.com/openai/openai-python/blob/main/examples/realtime/push_to_talk_app.py) for a fully fledged example. ### Realtime error handling [Permalink: Realtime error handling](https://github.com/openai/openai-python#realtime-error-handling) Whenever an error occurs, the Realtime API will send an [`error` event](https://platform.openai.com/docs/guides/realtime-model-capabilities#error-handling) and the connection will stay open and remain usable. This means you need to handle it yourself, as _no errors are raised directly_ by the SDK when an `error` event comes in. ``` client = AsyncOpenAI() async with client.beta.realtime.connect(model="gpt-4o-realtime-preview") as connection: ... async for event in connection: if event.type == 'error': print(event.error.type) print(event.error.code) print(event.error.event_id) print(event.error.message) ``` ## Using types [Permalink: Using types](https://github.com/openai/openai-python#using-types) Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev/) which also provide helper methods for things like: - Serializing back into JSON, `model.to_json()` - Converting to a dictionary, `model.to_dict()` Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. ## Pagination [Permalink: Pagination](https://github.com/openai/openai-python#pagination) List methods in the OpenAI API are paginated. This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually: ``` from openai import OpenAI client = OpenAI() all_jobs = [] # Automatically fetches more pages as needed. for job in client.fine_tuning.jobs.list( limit=20, ): # Do something with job here all_jobs.append(job) print(all_jobs) ``` Or, asynchronously: ``` import asyncio from openai import AsyncOpenAI client = AsyncOpenAI() async def main() -> None: all_jobs = [] # Iterate through items across all pages, issuing requests as needed. async for job in client.fine_tuning.jobs.list( limit=20, ): all_jobs.append(job) print(all_jobs) asyncio.run(main()) ``` Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages: ``` first_page = await client.fine_tuning.jobs.list( limit=20, ) if first_page.has_next_page(): print(f"will fetch next page using these details: {first_page.next_page_info()}") next_page = await first_page.get_next_page() print(f"number of items we just fetched: {len(next_page.data)}") # Remove `await` for non-async usage. ``` Or just work directly with the returned data: ``` first_page = await client.fine_tuning.jobs.list( limit=20, ) print(f"next page cursor: {first_page.after}") # => "next page cursor: ..." for job in first_page.data: print(job.id) # Remove `await` for non-async usage. ``` ## Nested params [Permalink: Nested params](https://github.com/openai/openai-python#nested-params) Nested parameters are dictionaries, typed using `TypedDict`, for example: ``` from openai import OpenAI client = OpenAI() completion = client.chat.completions.create( messages=[\ {\ "role": "user",\ "content": "Can you generate an example json object describing a fruit?",\ }\ ], model="gpt-4o", response_format={"type": "json_object"}, ) ``` ## File uploads [Permalink: File uploads](https://github.com/openai/openai-python#file-uploads) Request parameters that correspond to file uploads can be passed as `bytes`, a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`. ``` from pathlib import Path from openai import OpenAI client = OpenAI() client.files.create( file=Path("input.jsonl"), purpose="fine-tune", ) ``` The async client uses the exact same interface. If you pass a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, the file contents will be read asynchronously automatically. ## Handling errors [Permalink: Handling errors](https://github.com/openai/openai-python#handling-errors) When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `openai.APIConnectionError` is raised. When the API returns a non-success status code (that is, 4xx or 5xx response), a subclass of `openai.APIStatusError` is raised, containing `status_code` and `response` properties. All errors inherit from `openai.APIError`. ``` import openai from openai import OpenAI client = OpenAI() try: client.fine_tuning.jobs.create( model="gpt-4o", training_file="file-abc123", ) except openai.APIConnectionError as e: print("The server could not be reached") print(e.__cause__) # an underlying Exception, likely raised within httpx. except openai.RateLimitError as e: print("A 429 status code was received; we should back off a bit.") except openai.APIStatusError as e: print("Another non-200-range status code was received") print(e.status_code) print(e.response) ``` Error codes are as follows: | Status Code | Error Type | | --- | --- | | 400 | `BadRequestError` | | 401 | `AuthenticationError` | | 403 | `PermissionDeniedError` | | 404 | `NotFoundError` | | 422 | `UnprocessableEntityError` | | 429 | `RateLimitError` | | >=500 | `InternalServerError` | | N/A | `APIConnectionError` | ## Request IDs [Permalink: Request IDs](https://github.com/openai/openai-python#request-ids) > For more information on debugging requests, see [these docs](https://platform.openai.com/docs/api-reference/debugging-requests) All object responses in the SDK provide a `_request_id` property which is added from the `x-request-id` response header so that you can quickly log failing requests and report them back to OpenAI. ``` completion = await client.chat.completions.create( messages=[{"role": "user", "content": "Say this is a test"}], model="gpt-4" ) print(completion._request_id) # req_123 ``` Note that unlike other properties that use an `_` prefix, the `_request_id` property _is_ public. Unless documented otherwise, _all_ other `_` prefix properties, methods and modules are _private_. Important If you need to access request IDs for failed requests you must catch the `APIStatusError` exception ``` import openai try: completion = await client.chat.completions.create( messages=[{"role": "user", "content": "Say this is a test"}], model="gpt-4" ) except openai.APIStatusError as exc: print(exc.request_id) # req_123 raise exc ``` ### Retries [Permalink: Retries](https://github.com/openai/openai-python#retries) Certain errors are automatically retried 2 times by default, with a short exponential backoff. Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal errors are all retried by default. You can use the `max_retries` option to configure or disable retry settings: ``` from openai import OpenAI # Configure the default for all requests: client = OpenAI( # default is 2 max_retries=0, ) # Or, configure per-request: client.with_options(max_retries=5).chat.completions.create( messages=[\ {\ "role": "user",\ "content": "How can I get the name of the current day in JavaScript?",\ }\ ], model="gpt-4o", ) ``` ### Timeouts [Permalink: Timeouts](https://github.com/openai/openai-python#timeouts) By default requests time out after 10 minutes. You can configure this with a `timeout` option, which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object: ``` from openai import OpenAI # Configure the default for all requests: client = OpenAI( # 20 seconds (default is 10 minutes) timeout=20.0, ) # More granular control: client = OpenAI( timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0), ) # Override per-request: client.with_options(timeout=5.0).chat.completions.create( messages=[\ {\ "role": "user",\ "content": "How can I list all files in a directory using Python?",\ }\ ], model="gpt-4o", ) ``` On timeout, an `APITimeoutError` is thrown. Note that requests that time out are [retried twice by default](https://github.com/openai/openai-python#retries). ## Advanced [Permalink: Advanced](https://github.com/openai/openai-python#advanced) ### Logging [Permalink: Logging](https://github.com/openai/openai-python#logging) We use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module. You can enable logging by setting the environment variable `OPENAI_LOG` to `info`. ``` $ export OPENAI_LOG=info ``` Or to `debug` for more verbose logging. ### How to tell whether `None` means `null` or missing [Permalink: How to tell whether None means null or missing](https://github.com/openai/openai-python#how-to-tell-whether-none-means-null-or-missing) In an API response, a field may be explicitly `null`, or missing entirely; in either case, its value is `None` in this library. You can differentiate the two cases with `.model_fields_set`: ``` if response.my_field is None: if 'my_field' not in response.model_fields_set: print('Got json like {}, without a "my_field" key present at all.') else: print('Got json like {"my_field": null}.') ``` ### Accessing raw response data (e.g. headers) [Permalink: Accessing raw response data (e.g. headers)](https://github.com/openai/openai-python#accessing-raw-response-data-eg-headers) The "raw" Response object can be accessed by prefixing `.with_raw_response.` to any HTTP method call, e.g., ``` from openai import OpenAI client = OpenAI() response = client.chat.completions.with_raw_response.create( messages=[{\ "role": "user",\ "content": "Say this is a test",\ }], model="gpt-4o", ) print(response.headers.get('X-My-Header')) completion = response.parse() # get the object that `chat.completions.create()` would have returned print(completion) ``` These methods return a [`LegacyAPIResponse`](https://github.com/openai/openai-python/tree/main/src/openai/_legacy_response.py) object. This is a legacy class as we're changing it slightly in the next major version. For the sync client this will mostly be the same with the exception of `content` & `text` will be methods instead of properties. In the async client, all methods will be async. A migration script will be provided & the migration in general should be smooth. #### `.with_streaming_response` [Permalink: .with_streaming_response](https://github.com/openai/openai-python#with_streaming_response) The above interface eagerly reads the full response body when you make the request, which may not always be what you want. To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. As such, `.with_streaming_response` methods return a different [`APIResponse`](https://github.com/openai/openai-python/tree/main/src/openai/_response.py) object, and the async client returns an [`AsyncAPIResponse`](https://github.com/openai/openai-python/tree/main/src/openai/_response.py) object. ``` with client.chat.completions.with_streaming_response.create( messages=[\ {\ "role": "user",\ "content": "Say this is a test",\ }\ ], model="gpt-4o", ) as response: print(response.headers.get("X-My-Header")) for line in response.iter_lines(): print(line) ``` The context manager is required so that the response will reliably be closed. ### Making custom/undocumented requests [Permalink: Making custom/undocumented requests](https://github.com/openai/openai-python#making-customundocumented-requests) This library is typed for convenient access to the documented API. If you need to access undocumented endpoints, params, or response properties, the library can still be used. #### Undocumented endpoints [Permalink: Undocumented endpoints](https://github.com/openai/openai-python#undocumented-endpoints) To make requests to undocumented endpoints, you can make requests using `client.get`, `client.post`, and other http verbs. Options on the client will be respected (such as retries) when making this request. ``` import httpx response = client.post( "/foo", cast_to=httpx.Response, body={"my_param": True}, ) print(response.headers.get("x-foo")) ``` #### Undocumented request params [Permalink: Undocumented request params](https://github.com/openai/openai-python#undocumented-request-params) If you want to explicitly send an extra param, you can do so with the `extra_query`, `extra_body`, and `extra_headers` request options. #### Undocumented response properties [Permalink: Undocumented response properties](https://github.com/openai/openai-python#undocumented-response-properties) To access undocumented response properties, you can access the extra fields like `response.unknown_prop`. You can also get all the extra fields on the Pydantic model as a dict with [`response.model_extra`](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel.model_extra). ### Configuring the HTTP client [Permalink: Configuring the HTTP client](https://github.com/openai/openai-python#configuring-the-http-client) You can directly override the [httpx client](https://www.python-httpx.org/api/#client) to customize it for your use case, including: - Support for [proxies](https://www.python-httpx.org/advanced/proxies/) - Custom [transports](https://www.python-httpx.org/advanced/transports/) - Additional [advanced](https://www.python-httpx.org/advanced/clients/) functionality ``` import httpx from openai import OpenAI, DefaultHttpxClient client = OpenAI( # Or use the `OPENAI_BASE_URL` env var base_url="http://my.test.server.example.com:8083/v1", http_client=DefaultHttpxClient( proxy="http://my.test.proxy.example.com", transport=httpx.HTTPTransport(local_address="0.0.0.0"), ), ) ``` You can also customize the client on a per-request basis by using `with_options()`: ``` client.with_options(http_client=DefaultHttpxClient(...)) ``` ### Managing HTTP resources [Permalink: Managing HTTP resources](https://github.com/openai/openai-python#managing-http-resources) By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting. ``` from openai import OpenAI with OpenAI() as client: # make requests here ... # HTTP client is now closed ``` ## Microsoft Azure OpenAI [Permalink: Microsoft Azure OpenAI](https://github.com/openai/openai-python#microsoft-azure-openai) To use this library with [Azure OpenAI](https://learn.microsoft.com/azure/ai-services/openai/overview), use the `AzureOpenAI` class instead of the `OpenAI` class. Important The Azure API shape differs from the core API shape which means that the static types for responses / params won't always be correct. ``` from openai import AzureOpenAI # gets the API Key from environment variable AZURE_OPENAI_API_KEY client = AzureOpenAI( # https://learn.microsoft.com/azure/ai-services/openai/reference#rest-api-versioning api_version="2023-07-01-preview", # https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal#create-a-resource azure_endpoint="https://example-endpoint.openai.azure.com", ) completion = client.chat.completions.create( model="deployment-name", # e.g. gpt-35-instant messages=[\ {\ "role": "user",\ "content": "How do I output all files in a directory using Python?",\ },\ ], ) print(completion.to_json()) ``` In addition to the options provided in the base `OpenAI` client, the following options are provided: - `azure_endpoint` (or the `AZURE_OPENAI_ENDPOINT` environment variable) - `azure_deployment` - `api_version` (or the `OPENAI_API_VERSION` environment variable) - `azure_ad_token` (or the `AZURE_OPENAI_AD_TOKEN` environment variable) - `azure_ad_token_provider` An example of using the client with Microsoft Entra ID (formerly known as Azure Active Directory) can be found [here](https://github.com/openai/openai-python/blob/main/examples/azure_ad.py). ## Versioning [Permalink: Versioning](https://github.com/openai/openai-python#versioning) This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: 1. Changes that only affect static types, without breaking runtime behavior. 2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_ 3. Changes that we do not expect to impact the vast majority of users in practice. We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. We are keen for your feedback; please open an [issue](https://www.github.com/openai/openai-python/issues) with questions, bugs, or suggestions. ### Determining the installed version [Permalink: Determining the installed version](https://github.com/openai/openai-python#determining-the-installed-version) If you've upgraded to the latest version but aren't seeing any new features you were expecting then your python environment is likely still using an older version. You can determine the version that is being used at runtime with: ``` import openai print(openai.__version__) ``` ## Requirements [Permalink: Requirements](https://github.com/openai/openai-python#requirements) Python 3.8 or higher. ## Contributing [Permalink: Contributing](https://github.com/openai/openai-python#contributing) See [the contributing documentation](https://github.com/openai/openai-python/blob/main/CONTRIBUTING.md). ## About The official Python library for the OpenAI API [pypi.org/project/openai/](https://pypi.org/project/openai/ "https://pypi.org/project/openai/") ### Topics [python](https://github.com/topics/python "Topic: python") [openai](https://github.com/topics/openai "Topic: openai") ### Resources [Readme](https://github.com/openai/openai-python#readme-ov-file) ### License [Apache-2.0 license](https://github.com/openai/openai-python#Apache-2.0-1-ov-file) ### Security policy [Security policy](https://github.com/openai/openai-python#security-ov-file) [Activity](https://github.com/openai/openai-python/activity) [Custom properties](https://github.com/openai/openai-python/custom-properties) ### Stars [**24.6k**\\ stars](https://github.com/openai/openai-python/stargazers) ### Watchers [**309**\\ watching](https://github.com/openai/openai-python/watchers) ### Forks [**3.6k**\\ forks](https://github.com/openai/openai-python/forks) [Report repository](https://github.com/contact/report-content?content_url=https%3A%2F%2Fgithub.com%2Fopenai%2Fopenai-python&report=openai+%28user%29) ## [Releases\ 241](https://github.com/openai/openai-python/releases) [v1.63.0\\ Latest\\ \\ Feb 13, 2025](https://github.com/openai/openai-python/releases/tag/v1.63.0) [\+ 240 releases](https://github.com/openai/openai-python/releases) ## [Contributors\ 131](https://github.com/openai/openai-python/graphs/contributors) - [![@stainless-bot](https://avatars.githubusercontent.com/u/107565488?s=64&v=4)](https://github.com/stainless-bot) - [![@stainless-app[bot]](https://avatars.githubusercontent.com/in/378072?s=64&v=4)](https://github.com/apps/stainless-app) - [![@RobertCraigie](https://avatars.githubusercontent.com/u/23125036?s=64&v=4)](https://github.com/RobertCraigie) - [![@hallacy](https://avatars.githubusercontent.com/u/1945079?s=64&v=4)](https://github.com/hallacy) - [![@rachellim](https://avatars.githubusercontent.com/u/9589037?s=64&v=4)](https://github.com/rachellim) - [![@logankilpatrick](https://avatars.githubusercontent.com/u/35577566?s=64&v=4)](https://github.com/logankilpatrick) - [![@ddeville](https://avatars.githubusercontent.com/u/356759?s=64&v=4)](https://github.com/ddeville) - [![@kristapratico](https://avatars.githubusercontent.com/u/31998003?s=64&v=4)](https://github.com/kristapratico) - [![@BorisPower](https://avatars.githubusercontent.com/u/81998504?s=64&v=4)](https://github.com/BorisPower) - [![@athyuttamre](https://avatars.githubusercontent.com/u/1485350?s=64&v=4)](https://github.com/athyuttamre) - [![@mpokrass](https://avatars.githubusercontent.com/u/5784632?s=64&v=4)](https://github.com/mpokrass) - [![@jhallard](https://avatars.githubusercontent.com/u/7551586?s=64&v=4)](https://github.com/jhallard) - [![@sorinsuciu-msft](https://avatars.githubusercontent.com/u/12627402?s=64&v=4)](https://github.com/sorinsuciu-msft) - [![@cmurtz-msft](https://avatars.githubusercontent.com/u/120655914?s=64&v=4)](https://github.com/cmurtz-msft) [\+ 117 contributors](https://github.com/openai/openai-python/graphs/contributors) ## Languages - [Python99.8%](https://github.com/openai/openai-python/search?l=python) - Other0.2% You can’t perform that action at this time. ================================================ FILE: codebase-architectures/.gitignore ================================================ # Python bytecode files **/__pycache__/ **/*.pyc **/*.pyo **/*.pyd **/.pytest_cache/ **/.coverage **/*.so **/.DS_Store ================================================ FILE: codebase-architectures/README.md ================================================ # Codebase Architectures This directory contains examples of different codebase architectures, each implemented with simple, runnable code. ## Architectures Included 1. **Vertical Slice Architecture** - Feature-oriented organization where each feature contains all its necessary components 2. **Layered (N-Tier or MVC) Architecture** - Separation of concerns by technical layer 3. **Pipeline (Sequential Flow) Architecture** - Linear processing stages for data transformation 4. **Atomic/Composable Architecture** - Hierarchical organization from atomic modules to capabilities to endpoints ## Running the Examples ### Python Examples ```bash cd uv run main.py ``` ### Node.js Examples ```bash cd node main.js # or if bun is available: bun run main.js ``` Each architecture directory contains its own README with specific details about the implementation. ================================================ FILE: codebase-architectures/atomic-composable-architecture/README.md ================================================ # Atomic/Composable Architecture This directory demonstrates an Atomic/Composable Architecture implementation with a simple notification system application. ## Structure ``` atomic-composable-architecture/ ├── atom/ # Smallest atomic reusable building blocks │ ├── auth.py # Authentication utilities │ ├── validation.py # Data validation functions │ └── notifications.py # Notification helpers │ ├── molecule/ # Combines multiple atoms into features │ ├── user_management.py # Uses auth + validation atoms │ └── alerting.py # Uses notifications + validation atoms │ └── organism/ # Combines molecules into user-facing APIs ├── user_api.py # Uses user_management molecule └── alerts_api.py # Uses alerting molecule ``` ## Explanation - **Atom**: Bottom-level reusable components that must remain general-purpose and independent. In a unrestricted codebase design: Atoms can only depend on other atoms. In a stricter version: Atoms can only depend on other atoms, not molecules or organisms. - **Molecule**: Compose atoms to build concrete functionality. Molecules can depend on multiple atoms, enabling reuse and rapid feature composition. - **Organism**: The highest level, combining molecules to create user-facing APIs or interfaces. ## Benefits - Maximizes code reuse and composability; **reduces duplication** and accelerates feature development. - Clear hierarchical structure makes it easy to reason about what building blocks are available. - Promotes small, focused, and easily understandable code units. ## Cons - Indirection introduced by composability can make dependency tracing challenging. - Understanding module usage patterns (what uses what) may require navigating through multiple files or explicit documentation. - Requires discipline and careful adherence to dependency rules to avoid cyclic or unintended dependencies. ## Running the Example ```bash uv run main.py ``` ================================================ FILE: codebase-architectures/atomic-composable-architecture/atom/auth.py ================================================ #!/usr/bin/env python3 """ Authentication module for the Atomic/Composable Architecture. This module provides atomic authentication utilities. """ import hashlib import os import time import uuid from typing import Dict, Optional, Tuple # In-memory user store for demonstration purposes # In a real application, this would be a database USER_STORE: Dict[str, Dict] = {} # In-memory token store for demonstration purposes TOKEN_STORE: Dict[str, Dict] = {} def hash_password(password: str, salt: Optional[str] = None) -> Tuple[str, str]: """ Hash a password with a salt for secure storage. Args: password: The password to hash salt: Optional salt, generated if not provided Returns: Tuple of (hashed_password, salt) """ if salt is None: salt = os.urandom(16).hex() # In a real application, use a more secure hashing algorithm like bcrypt hashed = hashlib.sha256((password + salt).encode()).hexdigest() return hashed, salt def verify_password(password: str, hashed_password: str, salt: str) -> bool: """ Verify a password against a stored hash. Args: password: The password to verify hashed_password: The stored hashed password salt: The salt used for hashing Returns: True if the password matches, False otherwise """ calculated_hash, _ = hash_password(password, salt) return calculated_hash == hashed_password def register_user(username: str, password: str, email: str) -> Dict: """ Register a new user. Args: username: The username for the new user password: The password for the new user email: The email for the new user Returns: User data dictionary Raises: ValueError: If the username already exists """ if username in USER_STORE: raise ValueError(f"Username '{username}' already exists") hashed_password, salt = hash_password(password) user_id = str(uuid.uuid4()) user_data = { "id": user_id, "username": username, "email": email, "hashed_password": hashed_password, "salt": salt, "created_at": time.time() } USER_STORE[username] = user_data return {k: v for k, v in user_data.items() if k not in ["hashed_password", "salt"]} def authenticate(username: str, password: str) -> Optional[Dict]: """ Authenticate a user with username and password. Args: username: The username to authenticate password: The password to authenticate Returns: User data dictionary if authentication succeeds, None otherwise """ if username not in USER_STORE: return None user_data = USER_STORE[username] if verify_password(password, user_data["hashed_password"], user_data["salt"]): return {k: v for k, v in user_data.items() if k not in ["hashed_password", "salt"]} return None def create_token(user_id: str, expires_in: int = 3600) -> str: """ Create an authentication token for a user. Args: user_id: The user ID to create a token for expires_in: Token expiration time in seconds Returns: Authentication token """ token = str(uuid.uuid4()) expiration = time.time() + expires_in TOKEN_STORE[token] = { "user_id": user_id, "expires_at": expiration } return token def validate_token(token: str) -> Optional[str]: """ Validate an authentication token. Args: token: The token to validate Returns: User ID if the token is valid, None otherwise """ if token not in TOKEN_STORE: return None token_data = TOKEN_STORE[token] if token_data["expires_at"] < time.time(): # Token expired, remove it del TOKEN_STORE[token] return None return token_data["user_id"] def revoke_token(token: str) -> bool: """ Revoke an authentication token. Args: token: The token to revoke Returns: True if the token was revoked, False if it didn't exist """ if token in TOKEN_STORE: del TOKEN_STORE[token] return True return False def get_user_by_id(user_id: str) -> Optional[Dict]: """ Get a user by ID. Args: user_id: The user ID to look up Returns: User data dictionary if found, None otherwise """ for user_data in USER_STORE.values(): if user_data["id"] == user_id: return {k: v for k, v in user_data.items() if k not in ["hashed_password", "salt"]} return None ================================================ FILE: codebase-architectures/atomic-composable-architecture/atom/notifications.py ================================================ #!/usr/bin/env python3 """ Notifications module for the Atomic/Composable Architecture. This module provides atomic notification utilities. """ import time from typing import Dict, List, Optional # In-memory notification store for demonstration purposes NOTIFICATION_STORE: Dict[str, List[Dict]] = {} # Notification templates TEMPLATES = { "welcome": "Welcome, {username}! Thank you for joining our platform.", "password_reset": "Your password has been reset. If you didn't request this, please contact support.", "new_login": "New login detected from {device} at {location}.", "alert": "{message}" } def create_notification(user_id: str, notification_type: str, data: Dict, is_read: bool = False) -> Dict: """ Create a notification for a user. Args: user_id: The ID of the user to notify notification_type: The type of notification data: Data to include in the notification is_read: Whether the notification has been read Returns: The created notification """ if user_id not in NOTIFICATION_STORE: NOTIFICATION_STORE[user_id] = [] # Get template or use alert template as fallback template = TEMPLATES.get(notification_type, TEMPLATES["alert"]) # Format message with provided data try: message = template.format(**data) except KeyError: # Fallback if template variables are missing message = f"Notification: {notification_type}" notification = { "id": str(len(NOTIFICATION_STORE[user_id]) + 1), "user_id": user_id, "type": notification_type, "message": message, "data": data, "is_read": is_read, "created_at": time.time() } NOTIFICATION_STORE[user_id].append(notification) return notification def get_user_notifications(user_id: str, unread_only: bool = False) -> List[Dict]: """ Get notifications for a user. Args: user_id: The ID of the user unread_only: Whether to return only unread notifications Returns: List of notifications """ if user_id not in NOTIFICATION_STORE: return [] if unread_only: return [n for n in NOTIFICATION_STORE[user_id] if not n["is_read"]] return NOTIFICATION_STORE[user_id] def mark_notification_as_read(user_id: str, notification_id: str) -> bool: """ Mark a notification as read. Args: user_id: The ID of the user notification_id: The ID of the notification Returns: True if the notification was marked as read, False otherwise """ if user_id not in NOTIFICATION_STORE: return False for notification in NOTIFICATION_STORE[user_id]: if notification["id"] == notification_id: notification["is_read"] = True return True return False def mark_all_notifications_as_read(user_id: str) -> int: """ Mark all notifications for a user as read. Args: user_id: The ID of the user Returns: Number of notifications marked as read """ if user_id not in NOTIFICATION_STORE: return 0 count = 0 for notification in NOTIFICATION_STORE[user_id]: if not notification["is_read"]: notification["is_read"] = True count += 1 return count def delete_notification(user_id: str, notification_id: str) -> bool: """ Delete a notification. Args: user_id: The ID of the user notification_id: The ID of the notification Returns: True if the notification was deleted, False otherwise """ if user_id not in NOTIFICATION_STORE: return False for i, notification in enumerate(NOTIFICATION_STORE[user_id]): if notification["id"] == notification_id: del NOTIFICATION_STORE[user_id][i] return True return False def send_email_notification(email: str, subject: str, message: str) -> bool: """ Send an email notification (mock implementation). Args: email: The recipient's email address subject: The email subject message: The email message Returns: True if the email was sent successfully (always True in this mock) """ # In a real application, this would send an actual email print(f"[EMAIL] To: {email}, Subject: {subject}") print(f"[EMAIL] Message: {message}") return True def send_sms_notification(phone_number: str, message: str) -> bool: """ Send an SMS notification (mock implementation). Args: phone_number: The recipient's phone number message: The SMS message Returns: True if the SMS was sent successfully (always True in this mock) """ # In a real application, this would send an actual SMS print(f"[SMS] To: {phone_number}") print(f"[SMS] Message: {message}") return True def create_alert(user_id: str, message: str, level: str = "info", data: Optional[Dict] = None) -> Dict: """ Create an alert notification. Args: user_id: The ID of the user to alert message: The alert message level: Alert level (info, warning, error) data: Additional data for the alert Returns: The created notification """ if data is None: data = {} data["message"] = message notification = create_notification( user_id=user_id, notification_type="alert", data={ **data, "level": level } ) return notification ================================================ FILE: codebase-architectures/atomic-composable-architecture/atom/validation.py ================================================ #!/usr/bin/env python3 """ Validation module for the Atomic/Composable Architecture. This module provides atomic validation utilities. """ import re from typing import Any, Dict, List, Optional, Union def validate_required_fields(data: Dict[str, Any], required_fields: List[str]) -> List[str]: """ Validate that all required fields are present in the data. Args: data: The data to validate required_fields: List of required field names Returns: List of missing field names, empty if all required fields are present """ return [field for field in required_fields if field not in data or data[field] is None] def validate_email(email: str) -> bool: """ Validate an email address format. Args: email: The email address to validate Returns: True if the email is valid, False otherwise """ # Simple regex for email validation # In a real application, consider using a more comprehensive validation pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' return bool(re.match(pattern, email)) def validate_string_length(value: str, min_length: int = 0, max_length: Optional[int] = None) -> bool: """ Validate that a string's length is within the specified range. Args: value: The string to validate min_length: Minimum allowed length max_length: Maximum allowed length, or None for no maximum Returns: True if the string length is valid, False otherwise """ if not isinstance(value, str): return False if len(value) < min_length: return False if max_length is not None and len(value) > max_length: return False return True def validate_numeric_range(value: Union[int, float], min_value: Optional[Union[int, float]] = None, max_value: Optional[Union[int, float]] = None) -> bool: """ Validate that a numeric value is within the specified range. Args: value: The numeric value to validate min_value: Minimum allowed value, or None for no minimum max_value: Maximum allowed value, or None for no maximum Returns: True if the value is within range, False otherwise """ if not isinstance(value, (int, float)): return False if min_value is not None and value < min_value: return False if max_value is not None and value > max_value: return False return True def validate_pattern(value: str, pattern: str) -> bool: """ Validate that a string matches a regular expression pattern. Args: value: The string to validate pattern: Regular expression pattern to match Returns: True if the string matches the pattern, False otherwise """ return bool(re.match(pattern, value)) def validate_username(username: str) -> bool: """ Validate a username format. Args: username: The username to validate Returns: True if the username is valid, False otherwise """ # Username must be 3-20 characters, alphanumeric with underscores pattern = r'^[a-zA-Z0-9_]{3,20}$' return bool(re.match(pattern, username)) def validate_password_strength(password: str) -> Dict[str, bool]: """ Validate password strength against multiple criteria. Args: password: The password to validate Returns: Dictionary with validation results for each criterion """ results = { "length": len(password) >= 8, "uppercase": bool(re.search(r'[A-Z]', password)), "lowercase": bool(re.search(r'[a-z]', password)), "digit": bool(re.search(r'\d', password)), "special_char": bool(re.search(r'[!@#$%^&*(),.?":{}|<>]', password)) } results["is_valid"] = all(results.values()) return results def validate_data(data: Dict[str, Any], schema: Dict[str, Dict[str, Any]]) -> Dict[str, List[str]]: """ Validate data against a schema. Args: data: The data to validate schema: Validation schema defining field types and constraints Returns: Dictionary mapping field names to lists of validation error messages """ errors: Dict[str, List[str]] = {} for field_name, field_schema in schema.items(): field_type = field_schema.get("type") required = field_schema.get("required", False) # Check if required field is missing if required and (field_name not in data or data[field_name] is None): errors.setdefault(field_name, []).append("Field is required") continue # Skip validation for optional fields that are not present if field_name not in data or data[field_name] is None: continue value = data[field_name] # Type validation if field_type == "string" and not isinstance(value, str): errors.setdefault(field_name, []).append("Must be a string") elif field_type == "number" and not isinstance(value, (int, float)): errors.setdefault(field_name, []).append("Must be a number") elif field_type == "integer" and not isinstance(value, int): errors.setdefault(field_name, []).append("Must be an integer") elif field_type == "boolean" and not isinstance(value, bool): errors.setdefault(field_name, []).append("Must be a boolean") elif field_type == "array" and not isinstance(value, list): errors.setdefault(field_name, []).append("Must be an array") elif field_type == "object" and not isinstance(value, dict): errors.setdefault(field_name, []).append("Must be an object") # String-specific validations if field_type == "string" and isinstance(value, str): min_length = field_schema.get("min_length") max_length = field_schema.get("max_length") pattern = field_schema.get("pattern") if min_length is not None and len(value) < min_length: errors.setdefault(field_name, []).append(f"Must be at least {min_length} characters") if max_length is not None and len(value) > max_length: errors.setdefault(field_name, []).append(f"Must be at most {max_length} characters") if pattern is not None and not re.match(pattern, value): errors.setdefault(field_name, []).append("Does not match required pattern") # Number-specific validations if field_type in ["number", "integer"] and isinstance(value, (int, float)): minimum = field_schema.get("minimum") maximum = field_schema.get("maximum") if minimum is not None and value < minimum: errors.setdefault(field_name, []).append(f"Must be at least {minimum}") if maximum is not None and value > maximum: errors.setdefault(field_name, []).append(f"Must be at most {maximum}") return errors ================================================ FILE: codebase-architectures/atomic-composable-architecture/main.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # ] # /// """ Main application entry point for the Atomic/Composable Architecture example. """ from organism.user_api import UserAPI from organism.alerts_api import AlertsAPI def display_header(text): """Display a header with the given text.""" print("\n" + "=" * 50) print(f" {text}") print("=" * 50) def display_response(response): """Display an API response.""" status = response["status"] message = response["message"] data = response["data"] if status == "success": print(f"✅ {message}") else: print(f"❌ {message}") if data: if isinstance(data, dict): for key, value in data.items(): if key == "user": print("\nUser:") for user_key, user_value in value.items(): print(f" {user_key}: {user_value}") elif key == "alerts": print("\nAlerts:") for i, alert in enumerate(value): print(f"\nAlert {i+1}:") print(f" Message: {alert['message']}") print(f" Type: {alert['type']}") print(f" Level: {alert['data'].get('level', 'N/A')}") print(f" Read: {'Yes' if alert['is_read'] else 'No'}") else: print(f"\n{key.capitalize()}:") if isinstance(value, dict): for sub_key, sub_value in value.items(): print(f" {sub_key}: {sub_value}") else: print(f" {value}") def main(): """Run the application.""" display_header("Atomic/Composable Architecture Example") # Register users display_header("Registering Users") register_response = UserAPI.register( username="johndoe", password="Password123!", email="john@example.com" ) display_response(register_response) register_response2 = UserAPI.register( username="janedoe", password="Secure456@", email="jane@example.com" ) display_response(register_response2) # Try to register with invalid data invalid_register = UserAPI.register( username="user", password="weak", email="invalid-email" ) display_response(invalid_register) # Login display_header("User Login") login_response = UserAPI.login( username="johndoe", password="Password123!" ) display_response(login_response) # Store token for later use if login_response["status"] == "success" and login_response["data"]: token = login_response["data"]["token"] # Get user profile display_header("User Profile") profile_response = UserAPI.get_profile(token) display_response(profile_response) # Update profile display_header("Updating Profile") update_response = UserAPI.update_profile( token=token, profile_data={"name": "John Doe", "location": "New York"} ) display_response(update_response) # Send alerts display_header("Sending Alerts") info_alert = AlertsAPI.send_alert( token=token, message="This is an informational alert", level="info" ) display_response(info_alert) warning_alert = AlertsAPI.send_alert( token=token, message="This is a warning alert", level="warning", email="john@example.com" ) display_response(warning_alert) error_alert = AlertsAPI.send_alert( token=token, message="This is an error alert", level="error", additional_data={"error_code": "E123", "source": "system"} ) display_response(error_alert) # Get alerts display_header("Getting Alerts") alerts_response = AlertsAPI.get_alerts(token) display_response(alerts_response) # Filter alerts by level display_header("Filtering Alerts by Level") warning_alerts = AlertsAPI.get_alerts(token, level="warning") display_response(warning_alerts) # Mark an alert as read if alerts_response["status"] == "success" and alerts_response["data"]: alerts = alerts_response["data"]["alerts"] if alerts: alert_id = alerts[0]["id"] display_header("Marking Alert as Read") mark_response = AlertsAPI.mark_as_read(token, alert_id) display_response(mark_response) # Get unread alerts display_header("Getting Unread Alerts") unread_response = AlertsAPI.get_alerts(token, unread_only=True) display_response(unread_response) # Mark all as read display_header("Marking All Alerts as Read") mark_all_response = AlertsAPI.mark_all_as_read(token) display_response(mark_all_response) # Delete an alert display_header("Deleting an Alert") delete_response = AlertsAPI.delete_alert(token, alert_id) display_response(delete_response) # Send system notification display_header("Sending System Notification") system_response = AlertsAPI.send_system_alert( token=token, user_id=profile_response["data"]["user"]["id"], notification_type="welcome", data={"username": "johndoe"}, email="john@example.com" ) display_response(system_response) # Logout display_header("User Logout") logout_response = UserAPI.logout(token) display_response(logout_response) # Try to use expired token display_header("Using Expired Token") expired_response = UserAPI.get_profile(token) display_response(expired_response) if __name__ == "__main__": main() ================================================ FILE: codebase-architectures/atomic-composable-architecture/molecule/alerting.py ================================================ #!/usr/bin/env python3 """ Alerting capability for the Atomic/Composable Architecture. This capability combines notifications and validation modules. """ from typing import Dict, List, Optional, Tuple from atom.notifications import ( create_notification, get_user_notifications, mark_notification_as_read, mark_all_notifications_as_read, delete_notification, send_email_notification, send_sms_notification, create_alert ) from atom.validation import ( validate_required_fields, validate_email, validate_string_length ) def send_user_alert(user_id: str, message: str, level: str = "info", email: Optional[str] = None, phone: Optional[str] = None, additional_data: Optional[Dict] = None) -> Tuple[bool, Dict]: """ Send an alert to a user through multiple channels. Args: user_id: The ID of the user to alert message: The alert message level: Alert level (info, warning, error) email: Optional email address to send the alert to phone: Optional phone number to send the alert to additional_data: Additional data for the alert Returns: Tuple of (success, result) with notification details """ # Validate required fields missing_fields = validate_required_fields( {"user_id": user_id, "message": message}, ["user_id", "message"] ) if missing_fields: return False, {"error": f"Missing required fields: {', '.join(missing_fields)}"} # Validate message length if not validate_string_length(message, min_length=1, max_length=500): return False, {"error": "Message must be between 1 and 500 characters"} # Validate level valid_levels = ["info", "warning", "error"] if level not in valid_levels: return False, {"error": f"Level must be one of: {', '.join(valid_levels)}"} # Create the alert notification notification = create_alert( user_id=user_id, message=message, level=level, data=additional_data ) # Send email if provided email_sent = False if email: if validate_email(email): subject = f"Alert: {level.capitalize()}" email_sent = send_email_notification(email, subject, message) else: return False, {"error": "Invalid email format"} # Send SMS if provided sms_sent = False if phone: sms_sent = send_sms_notification(phone, message) return True, { "notification": notification, "channels": { "in_app": True, "email": email_sent, "sms": sms_sent } } def get_user_alerts(user_id: str, unread_only: bool = False, level: Optional[str] = None) -> List[Dict]: """ Get alerts for a user with optional filtering. Args: user_id: The ID of the user unread_only: Whether to return only unread alerts level: Optional filter by alert level Returns: List of alert notifications """ # Get all notifications for the user notifications = get_user_notifications(user_id, unread_only) # Filter to only alert type notifications alerts = [n for n in notifications if n["type"] == "alert"] # Filter by level if specified if level: alerts = [a for a in alerts if a["data"].get("level") == level] return alerts def mark_alert_as_read(user_id: str, notification_id: str) -> bool: """ Mark an alert as read. Args: user_id: The ID of the user notification_id: The ID of the notification Returns: True if the alert was marked as read, False otherwise """ return mark_notification_as_read(user_id, notification_id) def mark_all_alerts_as_read(user_id: str) -> int: """ Mark all alerts for a user as read. Args: user_id: The ID of the user Returns: Number of alerts marked as read """ return mark_all_notifications_as_read(user_id) def delete_user_alert(user_id: str, notification_id: str) -> bool: """ Delete an alert. Args: user_id: The ID of the user notification_id: The ID of the notification Returns: True if the alert was deleted, False otherwise """ return delete_notification(user_id, notification_id) def send_system_notification(user_id: str, notification_type: str, data: Dict, email: Optional[str] = None) -> Tuple[bool, Dict]: """ Send a system notification to a user. Args: user_id: The ID of the user notification_type: The type of notification (welcome, password_reset, new_login) data: Data for the notification template email: Optional email address to send the notification to Returns: Tuple of (success, result) with notification details """ # Validate required fields missing_fields = validate_required_fields( {"user_id": user_id, "notification_type": notification_type}, ["user_id", "notification_type"] ) if missing_fields: return False, {"error": f"Missing required fields: {', '.join(missing_fields)}"} # Validate notification type valid_types = ["welcome", "password_reset", "new_login"] if notification_type not in valid_types: return False, {"error": f"Notification type must be one of: {', '.join(valid_types)}"} # Create the notification notification = create_notification( user_id=user_id, notification_type=notification_type, data=data ) # Send email if provided email_sent = False if email: if validate_email(email): # Get the notification message message = notification["message"] subject = f"Notification: {notification_type.replace('_', ' ').title()}" email_sent = send_email_notification(email, subject, message) else: return False, {"error": "Invalid email format"} return True, { "notification": notification, "channels": { "in_app": True, "email": email_sent } } ================================================ FILE: codebase-architectures/atomic-composable-architecture/molecule/user_management.py ================================================ #!/usr/bin/env python3 """ User management capability for the Atomic/Composable Architecture. This capability combines auth and validation modules. """ from typing import Dict, List, Optional, Tuple from atom.auth import ( register_user, authenticate, create_token, validate_token, revoke_token, get_user_by_id ) from atom.validation import ( validate_required_fields, validate_email, validate_username, validate_password_strength, validate_string_length ) def register_new_user(username: str, password: str, email: str) -> Tuple[bool, Dict]: """ Register a new user with validation. Args: username: The username for the new user password: The password for the new user email: The email for the new user Returns: Tuple of (success, result) where result is either user data or error messages """ # Validate required fields missing_fields = validate_required_fields( {"username": username, "password": password, "email": email}, ["username", "password", "email"] ) if missing_fields: return False, {"error": f"Missing required fields: {', '.join(missing_fields)}"} # Validate username if not validate_username(username): return False, {"error": "Username must be 3-20 characters, alphanumeric with underscores"} # Validate email if not validate_email(email): return False, {"error": "Invalid email format"} # Validate password strength password_validation = validate_password_strength(password) if not password_validation["is_valid"]: errors = [] if not password_validation["length"]: errors.append("Password must be at least 8 characters") if not password_validation["uppercase"]: errors.append("Password must contain at least one uppercase letter") if not password_validation["lowercase"]: errors.append("Password must contain at least one lowercase letter") if not password_validation["digit"]: errors.append("Password must contain at least one digit") if not password_validation["special_char"]: errors.append("Password must contain at least one special character") return False, {"error": errors} try: # Register the user user_data = register_user(username, password, email) return True, {"user": user_data} except ValueError as e: return False, {"error": str(e)} def login_user(username: str, password: str) -> Tuple[bool, Dict]: """ Login a user and create an authentication token. Args: username: The username to authenticate password: The password to authenticate Returns: Tuple of (success, result) where result contains user data and token or error message """ # Validate required fields missing_fields = validate_required_fields( {"username": username, "password": password}, ["username", "password"] ) if missing_fields: return False, {"error": f"Missing required fields: {', '.join(missing_fields)}"} # Authenticate the user user_data = authenticate(username, password) if not user_data: return False, {"error": "Invalid username or password"} # Create an authentication token token = create_token(user_data["id"]) return True, { "user": user_data, "token": token } def validate_user_token(token: str) -> Tuple[bool, Optional[Dict]]: """ Validate a user token and return user data. Args: token: The token to validate Returns: Tuple of (success, user_data) where user_data is None if validation fails """ if not token: return False, None user_id = validate_token(token) if not user_id: return False, None user_data = get_user_by_id(user_id) if not user_data: return False, None return True, user_data def logout_user(token: str) -> bool: """ Logout a user by revoking their token. Args: token: The token to revoke Returns: True if the token was revoked, False otherwise """ return revoke_token(token) def update_user_profile(user_id: str, profile_data: Dict) -> Tuple[bool, Dict]: """ Update a user's profile data. Args: user_id: The ID of the user to update profile_data: The profile data to update Returns: Tuple of (success, result) where result is either updated user data or error messages """ # Get the current user data user_data = get_user_by_id(user_id) if not user_data: return False, {"error": "User not found"} # Validate email if provided if "email" in profile_data: if not validate_email(profile_data["email"]): return False, {"error": "Invalid email format"} # In a real application, this would update the user in the database # For this mock, we'll just print the update print(f"[UPDATE] User {user_id} profile updated:") for key, value in profile_data.items(): print(f" {key}: {value}") # Return success with mock updated data updated_user = {**user_data, **profile_data} return True, {"user": updated_user} def change_password(user_id: str, current_password: str, new_password: str) -> Tuple[bool, Dict]: """ Change a user's password. Args: user_id: The ID of the user current_password: The current password new_password: The new password Returns: Tuple of (success, result) where result contains a success message or error message """ # Validate required fields missing_fields = validate_required_fields( {"current_password": current_password, "new_password": new_password}, ["current_password", "new_password"] ) if missing_fields: return False, {"error": f"Missing required fields: {', '.join(missing_fields)}"} # Validate new password strength password_validation = validate_password_strength(new_password) if not password_validation["is_valid"]: errors = [] if not password_validation["length"]: errors.append("Password must be at least 8 characters") if not password_validation["uppercase"]: errors.append("Password must contain at least one uppercase letter") if not password_validation["lowercase"]: errors.append("Password must contain at least one lowercase letter") if not password_validation["digit"]: errors.append("Password must contain at least one digit") if not password_validation["special_char"]: errors.append("Password must contain at least one special character") return False, {"error": errors} # In a real application, this would verify the current password and update it # For this mock, we'll just print the change print(f"[PASSWORD] User {user_id} password changed") return True, {"message": "Password changed successfully"} ================================================ FILE: codebase-architectures/atomic-composable-architecture/organism/alerts_api.py ================================================ #!/usr/bin/env python3 """ Alerts API endpoints for the Atomic/Composable Architecture. This module combines alerting capability with HTTP endpoints. """ from typing import Dict, List, Optional from molecule.alerting import ( send_user_alert, get_user_alerts, mark_alert_as_read, mark_all_alerts_as_read, delete_user_alert, send_system_notification ) from molecule.user_management import validate_user_token class AlertsAPI: """API endpoints for alerts management.""" @staticmethod def send_alert(token: str, message: str, level: str = "info", email: Optional[str] = None, phone: Optional[str] = None, additional_data: Optional[Dict] = None) -> Dict: """ Send an alert to a user. Args: token: Authentication token message: The alert message level: Alert level (info, warning, error) email: Optional email address to send the alert to phone: Optional phone number to send the alert to additional_data: Additional data for the alert Returns: Response with success status and alert details or error message """ # Validate token success, user_data = validate_user_token(token) if not success: return { "status": "error", "message": "Invalid or expired token", "data": None } # Send alert success, result = send_user_alert( user_id=user_data["id"], message=message, level=level, email=email, phone=phone, additional_data=additional_data ) if success: return { "status": "success", "message": "Alert sent successfully", "data": result } else: return { "status": "error", "message": result.get("error", "Failed to send alert"), "data": None } @staticmethod def get_alerts(token: str, unread_only: bool = False, level: Optional[str] = None) -> Dict: """ Get alerts for a user. Args: token: Authentication token unread_only: Whether to return only unread alerts level: Optional filter by alert level Returns: Response with success status and alerts or error message """ # Validate token success, user_data = validate_user_token(token) if not success: return { "status": "error", "message": "Invalid or expired token", "data": None } # Get alerts alerts = get_user_alerts( user_id=user_data["id"], unread_only=unread_only, level=level ) return { "status": "success", "message": f"Retrieved {len(alerts)} alerts", "data": {"alerts": alerts} } @staticmethod def mark_as_read(token: str, notification_id: str) -> Dict: """ Mark an alert as read. Args: token: Authentication token notification_id: The ID of the notification Returns: Response with success status or error message """ # Validate token success, user_data = validate_user_token(token) if not success: return { "status": "error", "message": "Invalid or expired token", "data": None } # Mark as read success = mark_alert_as_read(user_data["id"], notification_id) if success: return { "status": "success", "message": "Alert marked as read", "data": None } else: return { "status": "error", "message": "Alert not found", "data": None } @staticmethod def mark_all_as_read(token: str) -> Dict: """ Mark all alerts as read. Args: token: Authentication token Returns: Response with success status and count of alerts marked as read """ # Validate token success, user_data = validate_user_token(token) if not success: return { "status": "error", "message": "Invalid or expired token", "data": None } # Mark all as read count = mark_all_alerts_as_read(user_data["id"]) return { "status": "success", "message": f"Marked {count} alerts as read", "data": {"count": count} } @staticmethod def delete_alert(token: str, notification_id: str) -> Dict: """ Delete an alert. Args: token: Authentication token notification_id: The ID of the notification Returns: Response with success status or error message """ # Validate token success, user_data = validate_user_token(token) if not success: return { "status": "error", "message": "Invalid or expired token", "data": None } # Delete alert success = delete_user_alert(user_data["id"], notification_id) if success: return { "status": "success", "message": "Alert deleted successfully", "data": None } else: return { "status": "error", "message": "Alert not found", "data": None } @staticmethod def send_system_alert(token: str, user_id: str, notification_type: str, data: Dict, email: Optional[str] = None) -> Dict: """ Send a system notification to a user (admin function). Args: token: Authentication token (must be admin) user_id: The ID of the user to notify notification_type: The type of notification data: Data for the notification template email: Optional email address to send the notification to Returns: Response with success status and notification details or error message """ # Validate token (in a real app, would check if user is admin) success, admin_data = validate_user_token(token) if not success: return { "status": "error", "message": "Invalid or expired token", "data": None } # Send system notification success, result = send_system_notification( user_id=user_id, notification_type=notification_type, data=data, email=email ) if success: return { "status": "success", "message": "System notification sent successfully", "data": result } else: return { "status": "error", "message": result.get("error", "Failed to send system notification"), "data": None } ================================================ FILE: codebase-architectures/atomic-composable-architecture/organism/user_api.py ================================================ #!/usr/bin/env python3 """ User API endpoints for the Atomic/Composable Architecture. This module combines user_management capability with HTTP endpoints. """ from typing import Dict, Optional from molecule.user_management import ( register_new_user, login_user, validate_user_token, logout_user, update_user_profile, change_password ) class UserAPI: """API endpoints for user management.""" @staticmethod def register(username: str, password: str, email: str) -> Dict: """ Register a new user. Args: username: The username for the new user password: The password for the new user email: The email for the new user Returns: Response with success status and user data or error message """ success, result = register_new_user(username, password, email) if success: return { "status": "success", "message": "User registered successfully", "data": result } else: return { "status": "error", "message": result.get("error", "Registration failed"), "data": None } @staticmethod def login(username: str, password: str) -> Dict: """ Login a user. Args: username: The username to authenticate password: The password to authenticate Returns: Response with success status and user data with token or error message """ success, result = login_user(username, password) if success: return { "status": "success", "message": "Login successful", "data": result } else: return { "status": "error", "message": result.get("error", "Login failed"), "data": None } @staticmethod def get_profile(token: str) -> Dict: """ Get a user's profile. Args: token: Authentication token Returns: Response with success status and user data or error message """ success, user_data = validate_user_token(token) if success: return { "status": "success", "message": "Profile retrieved successfully", "data": {"user": user_data} } else: return { "status": "error", "message": "Invalid or expired token", "data": None } @staticmethod def logout(token: str) -> Dict: """ Logout a user. Args: token: Authentication token Returns: Response with success status """ success = logout_user(token) if success: return { "status": "success", "message": "Logout successful", "data": None } else: return { "status": "error", "message": "Invalid token", "data": None } @staticmethod def update_profile(token: str, profile_data: Dict) -> Dict: """ Update a user's profile. Args: token: Authentication token profile_data: The profile data to update Returns: Response with success status and updated user data or error message """ # Validate token success, user_data = validate_user_token(token) if not success: return { "status": "error", "message": "Invalid or expired token", "data": None } # Update profile success, result = update_user_profile(user_data["id"], profile_data) if success: return { "status": "success", "message": "Profile updated successfully", "data": result } else: return { "status": "error", "message": result.get("error", "Profile update failed"), "data": None } @staticmethod def change_password(token: str, current_password: str, new_password: str) -> Dict: """ Change a user's password. Args: token: Authentication token current_password: The current password new_password: The new password Returns: Response with success status or error message """ # Validate token success, user_data = validate_user_token(token) if not success: return { "status": "error", "message": "Invalid or expired token", "data": None } # Change password success, result = change_password(user_data["id"], current_password, new_password) if success: return { "status": "success", "message": "Password changed successfully", "data": None } else: return { "status": "error", "message": result.get("error", "Password change failed"), "data": None } ================================================ FILE: codebase-architectures/layered-architecture/README.md ================================================ # Layered (N-Tier or MVC) Architecture This directory demonstrates a Layered Architecture implementation with a simple product catalog application. ## Structure ``` layered-architecture/ ├── api/ # Interfaces (controllers or endpoints) │ ├── product_api.py │ └── category_api.py ├── services/ # Business logic layer │ ├── product_service.py │ └── category_service.py ├── models/ # Data models and domain objects │ ├── product.py │ └── category.py ├── data/ # Data access and persistence │ └── database.py ├── utils/ # Shared utilities │ └── logger.py └── main.py # Application entry point ``` ## Benefits - Clear separation of concerns aids contributions - Centralized shared logic promotes consistency and reduces duplication - Clear role signaling (e.g., service vs. API vs. data) ## Cons - Features spread across layers; context management can be challenging - Tight coupling may occur between layers, complicating cross-layer changes ## Running the Example ```bash uv run main.py ``` ================================================ FILE: codebase-architectures/layered-architecture/api/category_api.py ================================================ #!/usr/bin/env python3 """ Category API endpoints. """ from services.category_service import CategoryService from utils.logger import Logger, app_logger class CategoryAPI: """API endpoints for category management.""" @staticmethod def create_category(name, description=None): """Create a new category.""" try: category = CategoryService.create_category(name, description) return { "success": True, "message": "Category created successfully", "data": category } except ValueError as e: Logger.warning(app_logger, f"Validation error in create_category: {str(e)}") return { "success": False, "message": str(e) } except Exception as e: Logger.error(app_logger, f"Error in create_category: {str(e)}", exc_info=True) return { "success": False, "message": "An error occurred while creating the category" } @staticmethod def get_category(category_id): """Get a category by ID.""" try: category = CategoryService.get_category(category_id) if not category: return { "success": False, "message": f"Category with ID {category_id} not found" } return { "success": True, "data": category } except Exception as e: Logger.error(app_logger, f"Error in get_category: {str(e)}", exc_info=True) return { "success": False, "message": "An error occurred while retrieving the category" } @staticmethod def get_all_categories(): """Get all categories.""" try: categories = CategoryService.get_all_categories() return { "success": True, "data": categories } except Exception as e: Logger.error(app_logger, f"Error in get_all_categories: {str(e)}", exc_info=True) return { "success": False, "message": "An error occurred while retrieving categories" } @staticmethod def update_category(category_id, name=None, description=None): """Update a category.""" try: category = CategoryService.update_category(category_id, name, description) if not category: return { "success": False, "message": f"Category with ID {category_id} not found" } return { "success": True, "message": "Category updated successfully", "data": category } except ValueError as e: Logger.warning(app_logger, f"Validation error in update_category: {str(e)}") return { "success": False, "message": str(e) } except Exception as e: Logger.error(app_logger, f"Error in update_category: {str(e)}", exc_info=True) return { "success": False, "message": "An error occurred while updating the category" } @staticmethod def delete_category(category_id): """Delete a category.""" try: result = CategoryService.delete_category(category_id) if not result: return { "success": False, "message": f"Category with ID {category_id} not found" } return { "success": True, "message": "Category deleted successfully" } except ValueError as e: Logger.warning(app_logger, f"Validation error in delete_category: {str(e)}") return { "success": False, "message": str(e) } except Exception as e: Logger.error(app_logger, f"Error in delete_category: {str(e)}", exc_info=True) return { "success": False, "message": "An error occurred while deleting the category" } ================================================ FILE: codebase-architectures/layered-architecture/api/product_api.py ================================================ #!/usr/bin/env python3 """ Product API endpoints. """ from services.product_service import ProductService from utils.logger import Logger, app_logger class ProductAPI: """API endpoints for product management.""" @staticmethod def create_product(name, price, category_id=None, description=None, sku=None): """Create a new product.""" try: product = ProductService.create_product(name, price, category_id, description, sku) return { "success": True, "message": "Product created successfully", "data": product } except ValueError as e: Logger.warning(app_logger, f"Validation error in create_product: {str(e)}") return { "success": False, "message": str(e) } except Exception as e: Logger.error(app_logger, f"Error in create_product: {str(e)}", exc_info=True) return { "success": False, "message": "An error occurred while creating the product" } @staticmethod def get_product(product_id): """Get a product by ID.""" try: product = ProductService.get_product(product_id) if not product: return { "success": False, "message": f"Product with ID {product_id} not found" } return { "success": True, "data": product } except Exception as e: Logger.error(app_logger, f"Error in get_product: {str(e)}", exc_info=True) return { "success": False, "message": "An error occurred while retrieving the product" } @staticmethod def get_by_sku(sku): """Get a product by SKU.""" try: product = ProductService.get_by_sku(sku) if not product: return { "success": False, "message": f"Product with SKU '{sku}' not found" } return { "success": True, "data": product } except Exception as e: Logger.error(app_logger, f"Error in get_by_sku: {str(e)}", exc_info=True) return { "success": False, "message": "An error occurred while retrieving the product" } @staticmethod def get_all_products(): """Get all products.""" try: products = ProductService.get_all_products() return { "success": True, "data": products } except Exception as e: Logger.error(app_logger, f"Error in get_all_products: {str(e)}", exc_info=True) return { "success": False, "message": "An error occurred while retrieving products" } @staticmethod def get_products_by_category(category_id): """Get all products in a category.""" try: products = ProductService.get_products_by_category(category_id) return { "success": True, "data": products } except Exception as e: Logger.error(app_logger, f"Error in get_products_by_category: {str(e)}", exc_info=True) return { "success": False, "message": "An error occurred while retrieving products" } @staticmethod def update_product(product_id, name=None, price=None, category_id=None, description=None, sku=None): """Update a product.""" try: product = ProductService.update_product(product_id, name, price, category_id, description, sku) if not product: return { "success": False, "message": f"Product with ID {product_id} not found" } return { "success": True, "message": "Product updated successfully", "data": product } except ValueError as e: Logger.warning(app_logger, f"Validation error in update_product: {str(e)}") return { "success": False, "message": str(e) } except Exception as e: Logger.error(app_logger, f"Error in update_product: {str(e)}", exc_info=True) return { "success": False, "message": "An error occurred while updating the product" } @staticmethod def delete_product(product_id): """Delete a product.""" try: result = ProductService.delete_product(product_id) if not result: return { "success": False, "message": f"Product with ID {product_id} not found" } return { "success": True, "message": "Product deleted successfully" } except Exception as e: Logger.error(app_logger, f"Error in delete_product: {str(e)}", exc_info=True) return { "success": False, "message": "An error occurred while deleting the product" } ================================================ FILE: codebase-architectures/layered-architecture/data/database.py ================================================ #!/usr/bin/env python3 """ Database module for data persistence. """ import uuid from utils.logger import Logger, app_logger class InMemoryDatabase: """In-memory database implementation.""" def __init__(self): """Initialize the database.""" self.data = {} self.logger = Logger.get_logger("database") Logger.info(self.logger, "Database initialized") def create_table(self, table_name): """Create a new table if it doesn't exist.""" if table_name not in self.data: self.data[table_name] = {} Logger.info(self.logger, f"Table '{table_name}' created") def insert(self, table_name, item): """Insert an item into a table.""" if table_name not in self.data: self.create_table(table_name) # Generate ID if not provided if "id" not in item: item["id"] = str(uuid.uuid4()) self.data[table_name][item["id"]] = item Logger.info(self.logger, f"Item inserted into '{table_name}' with ID {item['id']}") return item def get(self, table_name, item_id): """Get an item from a table by ID.""" if table_name not in self.data or item_id not in self.data[table_name]: Logger.warning(self.logger, f"Item with ID {item_id} not found in '{table_name}'") return None Logger.debug(self.logger, f"Retrieved item with ID {item_id} from '{table_name}'") return self.data[table_name][item_id] def get_all(self, table_name): """Get all items from a table.""" if table_name not in self.data: Logger.warning(self.logger, f"Table '{table_name}' not found") return [] items = list(self.data[table_name].values()) Logger.debug(self.logger, f"Retrieved {len(items)} items from '{table_name}'") return items def update(self, table_name, item_id, item): """Update an item in a table.""" if table_name not in self.data or item_id not in self.data[table_name]: Logger.warning(self.logger, f"Cannot update: Item with ID {item_id} not found in '{table_name}'") return None # Ensure ID remains the same item["id"] = item_id self.data[table_name][item_id] = item Logger.info(self.logger, f"Updated item with ID {item_id} in '{table_name}'") return item def delete(self, table_name, item_id): """Delete an item from a table.""" if table_name not in self.data or item_id not in self.data[table_name]: Logger.warning(self.logger, f"Cannot delete: Item with ID {item_id} not found in '{table_name}'") return False del self.data[table_name][item_id] Logger.info(self.logger, f"Deleted item with ID {item_id} from '{table_name}'") return True def query(self, table_name, filter_func): """Query items from a table using a filter function.""" if table_name not in self.data: Logger.warning(self.logger, f"Table '{table_name}' not found for query") return [] items = list(self.data[table_name].values()) filtered_items = [item for item in items if filter_func(item)] Logger.debug(self.logger, f"Query returned {len(filtered_items)} items from '{table_name}'") return filtered_items # Create a singleton database instance db = InMemoryDatabase() ================================================ FILE: codebase-architectures/layered-architecture/main.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # ] # /// """ Main application entry point for the Layered Architecture example. """ from api.category_api import CategoryAPI from api.product_api import ProductAPI from utils.logger import app_logger, Logger def display_header(text): """Display a header with the given text.""" print("\n" + "=" * 50) print(f" {text}") print("=" * 50) def display_result(result): """Display a result.""" if result.get("success"): print("✅ " + result.get("message", "Operation successful")) if "data" in result: data = result["data"] if isinstance(data, list): for item in data: print_item(item) else: print_item(data) else: print("❌ " + result.get("message", "Operation failed")) def print_item(item): """Print an item.""" if isinstance(item, dict): for key, value in item.items(): if key not in ["created_at", "updated_at"]: print(f" {key}: {value}") print() def main(): """Run the application.""" Logger.info(app_logger, "Starting Layered Architecture Example") display_header("Layered Architecture Example") # Create categories display_header("Creating Categories") electronics_result = CategoryAPI.create_category("Electronics", "Electronic devices and gadgets") display_result(electronics_result) books_result = CategoryAPI.create_category("Books", "Books and e-books") display_result(books_result) clothing_result = CategoryAPI.create_category("Clothing", "Apparel and accessories") display_result(clothing_result) # Try to create a duplicate category duplicate_result = CategoryAPI.create_category("Electronics", "Duplicate category") display_result(duplicate_result) # Get all categories display_header("All Categories") categories_result = CategoryAPI.get_all_categories() display_result(categories_result) # Create products display_header("Creating Products") # Get category IDs categories = categories_result["data"] electronics_id = next((c["id"] for c in categories if c["name"] == "Electronics"), None) books_id = next((c["id"] for c in categories if c["name"] == "Books"), None) # Create products laptop_result = ProductAPI.create_product( "Laptop", 999.99, electronics_id, "High-performance laptop", "TECH-001" ) display_result(laptop_result) phone_result = ProductAPI.create_product( "Smartphone", 499.99, electronics_id, "Latest smartphone model", "TECH-002" ) display_result(phone_result) book_result = ProductAPI.create_product( "Programming Book", 29.99, books_id, "Learn programming with this book", "BOOK-001" ) display_result(book_result) # Try to create a product with invalid price invalid_result = ProductAPI.create_product( "Invalid Product", "not-a-price", electronics_id ) display_result(invalid_result) # Get products by category display_header("Electronics Products") electronics_products = ProductAPI.get_products_by_category(electronics_id) display_result(electronics_products) # Update a product display_header("Updating a Product") if laptop_result.get("success") and "data" in laptop_result: laptop_id = laptop_result["data"]["id"] update_result = ProductAPI.update_product( laptop_id, price=899.99, description="High-performance laptop with discount" ) display_result(update_result) # Try to delete a category with products display_header("Trying to Delete a Category with Products") delete_result = CategoryAPI.delete_category(electronics_id) display_result(delete_result) # Delete a product display_header("Deleting a Product") if phone_result.get("success") and "data" in phone_result: phone_id = phone_result["data"]["id"] delete_product_result = ProductAPI.delete_product(phone_id) display_result(delete_product_result) # Get all products display_header("All Remaining Products") all_products = ProductAPI.get_all_products() display_result(all_products) Logger.info(app_logger, "Layered Architecture Example completed") if __name__ == "__main__": main() ================================================ FILE: codebase-architectures/layered-architecture/models/category.py ================================================ #!/usr/bin/env python3 """ Category model definition. """ from datetime import datetime class Category: """Category model representing a product category.""" def __init__(self, name, description=None, id=None): """Initialize a category.""" self.id = id self.name = name self.description = description self.created_at = datetime.now().isoformat() self.updated_at = self.created_at def to_dict(self): """Convert category to dictionary.""" return { "id": self.id, "name": self.name, "description": self.description, "created_at": self.created_at, "updated_at": self.updated_at } @classmethod def from_dict(cls, data): """Create a category from dictionary.""" category = cls( name=data["name"], description=data.get("description"), id=data.get("id") ) category.created_at = data.get("created_at", category.created_at) category.updated_at = data.get("updated_at", category.updated_at) return category ================================================ FILE: codebase-architectures/layered-architecture/models/product.py ================================================ #!/usr/bin/env python3 """ Product model definition. """ from datetime import datetime class Product: """Product model representing a product in the catalog.""" def __init__(self, name, price, category_id=None, description=None, sku=None, id=None): """Initialize a product.""" self.id = id self.name = name self.price = price self.category_id = category_id self.description = description self.sku = sku self.created_at = datetime.now().isoformat() self.updated_at = self.created_at def to_dict(self): """Convert product to dictionary.""" return { "id": self.id, "name": self.name, "price": self.price, "category_id": self.category_id, "description": self.description, "sku": self.sku, "created_at": self.created_at, "updated_at": self.updated_at } @classmethod def from_dict(cls, data): """Create a product from dictionary.""" product = cls( name=data["name"], price=data["price"], category_id=data.get("category_id"), description=data.get("description"), sku=data.get("sku"), id=data.get("id") ) product.created_at = data.get("created_at", product.created_at) product.updated_at = data.get("updated_at", product.updated_at) return product ================================================ FILE: codebase-architectures/layered-architecture/services/category_service.py ================================================ #!/usr/bin/env python3 """ Category service containing business logic for category management. """ from datetime import datetime from data.database import db from models.category import Category from utils.logger import Logger, app_logger class CategoryService: """Service for managing categories.""" @staticmethod def create_category(name, description=None): """Create a new category.""" try: # Validate category name if not name or not isinstance(name, str): raise ValueError("Category name is required and must be a string") # Check if category with same name already exists existing_categories = db.query("categories", lambda c: c["name"].lower() == name.lower()) if existing_categories: raise ValueError(f"Category with name '{name}' already exists") # Create and save category category = Category(name=name, description=description) saved_category = db.insert("categories", category.to_dict()) Logger.info(app_logger, f"Created category: {name}") return saved_category except Exception as e: Logger.error(app_logger, f"Error creating category: {str(e)}", exc_info=True) raise @staticmethod def get_category(category_id): """Get a category by ID.""" try: category_data = db.get("categories", category_id) if not category_data: Logger.warning(app_logger, f"Category not found: {category_id}") return None return category_data except Exception as e: Logger.error(app_logger, f"Error getting category: {str(e)}", exc_info=True) raise @staticmethod def get_all_categories(): """Get all categories.""" try: categories = db.get_all("categories") Logger.info(app_logger, f"Retrieved {len(categories)} categories") return categories except Exception as e: Logger.error(app_logger, f"Error getting all categories: {str(e)}", exc_info=True) raise @staticmethod def update_category(category_id, name=None, description=None): """Update a category.""" try: # Get existing category category_data = db.get("categories", category_id) if not category_data: Logger.warning(app_logger, f"Cannot update: Category not found: {category_id}") return None # Check if new name already exists if name and name != category_data["name"]: existing_categories = db.query("categories", lambda c: c["name"].lower() == name.lower() and c["id"] != category_id) if existing_categories: raise ValueError(f"Category with name '{name}' already exists") # Update fields if name: category_data["name"] = name if description is not None: category_data["description"] = description # Update timestamp category_data["updated_at"] = datetime.now().isoformat() # Save to database updated_category = db.update("categories", category_id, category_data) Logger.info(app_logger, f"Updated category: {category_id}") return updated_category except Exception as e: Logger.error(app_logger, f"Error updating category: {str(e)}", exc_info=True) raise @staticmethod def delete_category(category_id): """Delete a category.""" try: # Check if category exists category_data = db.get("categories", category_id) if not category_data: Logger.warning(app_logger, f"Cannot delete: Category not found: {category_id}") return False # Check if category has products products = db.query("products", lambda p: p["category_id"] == category_id) if products: raise ValueError(f"Cannot delete category: {len(products)} products are associated with this category") # Delete category result = db.delete("categories", category_id) Logger.info(app_logger, f"Deleted category: {category_id}") return result except Exception as e: Logger.error(app_logger, f"Error deleting category: {str(e)}", exc_info=True) raise ================================================ FILE: codebase-architectures/layered-architecture/services/product_service.py ================================================ #!/usr/bin/env python3 """ Product service containing business logic for product management. """ from datetime import datetime from data.database import db from models.product import Product from utils.logger import Logger, app_logger class ProductService: """Service for managing products.""" @staticmethod def create_product(name, price, category_id=None, description=None, sku=None): """Create a new product.""" try: # Validate product data if not name or not isinstance(name, str): raise ValueError("Product name is required and must be a string") try: price = float(price) if price < 0: raise ValueError() except (ValueError, TypeError): raise ValueError("Price must be a positive number") # Validate category if provided if category_id: category = db.get("categories", category_id) if not category: raise ValueError(f"Category with ID {category_id} not found") # Validate SKU if provided if sku: existing_products = db.query("products", lambda p: p["sku"] == sku) if existing_products: raise ValueError(f"Product with SKU '{sku}' already exists") # Create and save product product = Product( name=name, price=price, category_id=category_id, description=description, sku=sku ) saved_product = db.insert("products", product.to_dict()) Logger.info(app_logger, f"Created product: {name}") return saved_product except Exception as e: Logger.error(app_logger, f"Error creating product: {str(e)}", exc_info=True) raise @staticmethod def get_product(product_id): """Get a product by ID.""" try: product_data = db.get("products", product_id) if not product_data: Logger.warning(app_logger, f"Product not found: {product_id}") return None return product_data except Exception as e: Logger.error(app_logger, f"Error getting product: {str(e)}", exc_info=True) raise @staticmethod def get_by_sku(sku): """Get a product by SKU.""" try: products = db.query("products", lambda p: p["sku"] == sku) if not products: Logger.warning(app_logger, f"Product with SKU '{sku}' not found") return None return products[0] except Exception as e: Logger.error(app_logger, f"Error getting product by SKU: {str(e)}", exc_info=True) raise @staticmethod def get_all_products(): """Get all products.""" try: products = db.get_all("products") Logger.info(app_logger, f"Retrieved {len(products)} products") return products except Exception as e: Logger.error(app_logger, f"Error getting all products: {str(e)}", exc_info=True) raise @staticmethod def get_products_by_category(category_id): """Get all products in a category.""" try: products = db.query("products", lambda p: p["category_id"] == category_id) Logger.info(app_logger, f"Retrieved {len(products)} products for category {category_id}") return products except Exception as e: Logger.error(app_logger, f"Error getting products by category: {str(e)}", exc_info=True) raise @staticmethod def update_product(product_id, name=None, price=None, category_id=None, description=None, sku=None): """Update a product.""" try: # Get existing product product_data = db.get("products", product_id) if not product_data: Logger.warning(app_logger, f"Cannot update: Product not found: {product_id}") return None # Validate price if provided if price is not None: try: price = float(price) if price < 0: raise ValueError() except (ValueError, TypeError): raise ValueError("Price must be a positive number") # Validate category if provided if category_id: category = db.get("categories", category_id) if not category: raise ValueError(f"Category with ID {category_id} not found") # Validate SKU if provided if sku and sku != product_data["sku"]: existing_products = db.query("products", lambda p: p["sku"] == sku and p["id"] != product_id) if existing_products: raise ValueError(f"Product with SKU '{sku}' already exists") # Update fields if name: product_data["name"] = name if price is not None: product_data["price"] = price if category_id is not None: product_data["category_id"] = category_id if description is not None: product_data["description"] = description if sku is not None: product_data["sku"] = sku # Update timestamp product_data["updated_at"] = datetime.now().isoformat() # Save to database updated_product = db.update("products", product_id, product_data) Logger.info(app_logger, f"Updated product: {product_id}") return updated_product except Exception as e: Logger.error(app_logger, f"Error updating product: {str(e)}", exc_info=True) raise @staticmethod def delete_product(product_id): """Delete a product.""" try: # Check if product exists product_data = db.get("products", product_id) if not product_data: Logger.warning(app_logger, f"Cannot delete: Product not found: {product_id}") return False # Delete product result = db.delete("products", product_id) Logger.info(app_logger, f"Deleted product: {product_id}") return result except Exception as e: Logger.error(app_logger, f"Error deleting product: {str(e)}", exc_info=True) raise ================================================ FILE: codebase-architectures/layered-architecture/utils/logger.py ================================================ #!/usr/bin/env python3 """ Logger utility for the application. """ import logging from datetime import datetime # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) class Logger: """Logger class for application logging.""" @staticmethod def get_logger(name): """Get a logger instance for the given name.""" return logging.getLogger(name) @staticmethod def info(logger, message): """Log an info message.""" logger.info(message) @staticmethod def error(logger, message, exc_info=None): """Log an error message.""" logger.error(message, exc_info=exc_info) @staticmethod def warning(logger, message): """Log a warning message.""" logger.warning(message) @staticmethod def debug(logger, message): """Log a debug message.""" logger.debug(message) # Create a default logger app_logger = Logger.get_logger("app") ================================================ FILE: codebase-architectures/pipeline-architecture/README.md ================================================ # Pipeline (Sequential Flow) Architecture This directory demonstrates a Pipeline Architecture implementation with a simple data processing application. ## Structure ``` pipeline-architecture/ ├── steps/ # Composable pipeline steps │ ├── input_stage.py # Input parsing and preparation │ ├── processing_stage.py # Core computation or transformation │ └── output_stage.py # Final formatting or response handling ├── pipeline_manager/ # Pipeline orchestration │ ├── pipeline_manager.py # Base pipeline manager │ └── data_pipeline.py # Data processing pipeline implementation └── shared/ └── utilities.py # Common utilities across pipeline ``` This architecture follows a more functional approach, where: - Steps are composable, independent units that can be mixed and matched - Pipeline managers orchestrate the flow between steps - Different pipeline implementations can be created for specific use cases - Each step focuses on a single responsibility and can be tested in isolation ## Benefits - Clearly defined linear execution simplifies reasoning and debugging - Easy to scale or optimize individual pipeline stages independently - Facilitates predictable context management ## Cons - Rigid linearity limits branching and complex decision-making scenarios - Major workflow changes can require extensive pipeline refactoring ## Running the Example ```bash uv run main.py ``` This example demonstrates a data processing pipeline that: 1. Reads and validates input data 2. Processes and transforms the data 3. Formats and outputs the results ================================================ FILE: codebase-architectures/pipeline-architecture/data/.gitkeep ================================================ # This directory will store sample data files ================================================ FILE: codebase-architectures/pipeline-architecture/data/sales_data.json ================================================ [ { "id": "S001", "product": "Laptop", "category": "Electronics", "price": 1299.99, "quantity": 5, "date": "2025-01-15", "customer": "ABC Corp", "discount": 0.1 }, { "id": "S002", "product": "Smartphone", "category": "Electronics", "price": 899.99, "quantity": 10, "date": "2025-01-20", "customer": "XYZ Ltd", "discount": 0.05 }, { "id": "S003", "product": "Office Chair", "category": "Furniture", "price": 249.99, "quantity": 8, "date": "2025-01-22", "customer": "123 Industries", "discount": 0.0 }, { "id": "S004", "product": "Desk", "category": "Furniture", "price": 349.99, "quantity": 4, "date": "2025-01-25", "customer": "ABC Corp", "discount": 0.15 }, { "id": "S005", "product": "Monitor", "category": "Electronics", "price": 499.99, "quantity": 12, "date": "2025-01-30", "customer": "XYZ Ltd", "discount": 0.1 }, { "id": "S006", "product": "Printer", "category": "Electronics", "price": 299.99, "quantity": 3, "date": "2025-02-05", "customer": "123 Industries", "discount": 0.0 }, { "id": "S007", "product": "Bookshelf", "category": "Furniture", "price": 199.99, "quantity": 6, "date": "2025-02-10", "customer": "ABC Corp", "discount": 0.05 } ] ================================================ FILE: codebase-architectures/pipeline-architecture/main.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # ] # /// """ Main application entry point for the Pipeline Architecture example. """ import os import json from steps.input_stage import InputStage from steps.processing_stage import ProcessingStage from steps.output_stage import OutputStage from pipeline_manager.data_pipeline import DataProcessingPipeline from shared.utilities import format_currency, format_percentage def create_sample_data(): """Create sample sales data for the pipeline example.""" # Create output directory if it doesn't exist os.makedirs("./data", exist_ok=True) # Sample sales data sales_data = [ { "id": "S001", "product": "Laptop", "category": "Electronics", "price": 1299.99, "quantity": 5, "date": "2025-01-15", "customer": "ABC Corp", "discount": 0.1 }, { "id": "S002", "product": "Smartphone", "category": "Electronics", "price": 899.99, "quantity": 10, "date": "2025-01-20", "customer": "XYZ Ltd", "discount": 0.05 }, { "id": "S003", "product": "Office Chair", "category": "Furniture", "price": 249.99, "quantity": 8, "date": "2025-01-22", "customer": "123 Industries", "discount": 0.0 }, { "id": "S004", "product": "Desk", "category": "Furniture", "price": 349.99, "quantity": 4, "date": "2025-01-25", "customer": "ABC Corp", "discount": 0.15 }, { "id": "S005", "product": "Monitor", "category": "Electronics", "price": 499.99, "quantity": 12, "date": "2025-01-30", "customer": "XYZ Ltd", "discount": 0.1 }, { "id": "S006", "product": "Printer", "category": "Electronics", "price": 299.99, "quantity": 3, "date": "2025-02-05", "customer": "123 Industries", "discount": 0.0 }, { "id": "S007", "product": "Bookshelf", "category": "Furniture", "price": 199.99, "quantity": 6, "date": "2025-02-10", "customer": "ABC Corp", "discount": 0.05 } ] # Save to file with open("./data/sales_data.json", "w") as file: json.dump(sales_data, file, indent=2) print(f"Created sample data file: ./data/sales_data.json") return "./data/sales_data.json" def main(): """Run the pipeline architecture example.""" print("\n===== Pipeline Architecture Example =====") # Create sample data data_file = create_sample_data() # Create pipeline stages input_stage = InputStage() processing_stage = ProcessingStage() output_stage = OutputStage() # Create and configure pipeline pipeline = DataProcessingPipeline("Sales Data Analysis Pipeline") # Add stages pipeline.add_stage("input", input_stage) pipeline.add_stage("processing", processing_stage) pipeline.add_stage("output", output_stage) # Configure input pipeline.configure_input( source=data_file, source_type="json", required_fields=["id", "product", "price", "quantity"] ) # Configure processing pipeline.configure_processing({ "calculate_statistics": True, "numeric_fields": ["price", "quantity", "discount"], "filters": [ { "filter_func": lambda item: item["price"] * item["quantity"] > 1000, "description": "High-value sales (>$1000)" } ], "transformations": { "price": lambda price: format_currency(price), "discount": lambda discount: format_percentage(discount) }, "transformation_description": "Format price as currency and discount as percentage" }) # Configure output pipeline.configure_output({ "format_summary": True, "format_detailed": True, "print_results": True, "print_output_type": "summary", "save_to_file": [ { "format": "json", "dir": "./output", "filename": "sales_analysis.json" } ] }) # Run the pipeline result = pipeline.run() print("\n===== Pipeline Execution Complete =====") print(f"Pipeline status: {result['metadata']['status']}") print(f"Execution time: {result['metadata']['execution_time_seconds']:.2f} seconds") # Show output file location if saved if "stages" in result and len(result["stages"]) > 0: output_stage_name = result["stages"][-1]["name"] if output_stage_name in pipeline.results: output_result = pipeline.results[output_stage_name] if "metadata" in output_result and "output_files" in output_result["metadata"]: print("\nOutput files:") for output_file in output_result["metadata"]["output_files"]: print(f"- {output_file['path']} ({output_file['format']})") if __name__ == "__main__": main() ================================================ FILE: codebase-architectures/pipeline-architecture/output/.gitkeep ================================================ # This directory will store output files generated by the pipeline ================================================ FILE: codebase-architectures/pipeline-architecture/output/sales_analysis.json ================================================ { "report_type": "detailed", "generated_at": "2025-03-17T14:25:07.162838", "data_source": "./data/sales_data.json", "record_count": 6, "data": [ { "id": "S001", "product": "Laptop", "category": "Electronics", "price": "$1299.99", "quantity": 5, "date": "2025-01-15", "customer": "ABC Corp", "discount": "10.0%" }, { "id": "S002", "product": "Smartphone", "category": "Electronics", "price": "$899.99", "quantity": 10, "date": "2025-01-20", "customer": "XYZ Ltd", "discount": "5.0%" }, { "id": "S003", "product": "Office Chair", "category": "Furniture", "price": "$249.99", "quantity": 8, "date": "2025-01-22", "customer": "123 Industries", "discount": "0.0%" }, { "id": "S004", "product": "Desk", "category": "Furniture", "price": "$349.99", "quantity": 4, "date": "2025-01-25", "customer": "ABC Corp", "discount": "15.0%" }, { "id": "S005", "product": "Monitor", "category": "Electronics", "price": "$499.99", "quantity": 12, "date": "2025-01-30", "customer": "XYZ Ltd", "discount": "10.0%" }, { "id": "S007", "product": "Bookshelf", "category": "Furniture", "price": "$199.99", "quantity": 6, "date": "2025-02-10", "customer": "ABC Corp", "discount": "5.0%" } ], "analysis": { "statistics": { "price": { "count": 7, "min": 199.99, "max": 1299.99, "sum": 3799.9300000000003, "mean": 542.8471428571429, "median": 349.99, "std_dev": 408.68546527104377 }, "quantity": { "count": 7, "min": 3, "max": 12, "sum": 48, "mean": 6.857142857142857, "median": 6, "std_dev": 3.2877840272018797 }, "discount": { "count": 7, "min": 0.0, "max": 0.15, "sum": 0.45, "mean": 0.0642857142857143, "median": 0.05, "std_dev": 0.05563486402641868 } } }, "processing_info": { "steps": [ "calculate_statistics", "filter_data", "transform_fields" ], "filters": [ { "description": "High-value sales (>$1000)", "original_count": 7, "filtered_count": 6, "removed_count": 1 } ], "transformations": [ { "description": "Format price as currency and discount as percentage", "fields_transformed": [ "price", "discount" ] } ], "processing_time_seconds": 0.000299 } } ================================================ FILE: codebase-architectures/pipeline-architecture/pipeline_manager/data_pipeline.py ================================================ #!/usr/bin/env python3 """ Data processing pipeline implementation for the pipeline architecture. This module provides a specific implementation of the pipeline manager for data processing. """ from pipeline_manager.pipeline_manager import PipelineManager class DataProcessingPipeline(PipelineManager): """Specific implementation of a data processing pipeline.""" def __init__(self, name="Data Processing Pipeline"): """Initialize the data processing pipeline.""" super().__init__(name) def _execute_first_stage(self, input_stage): """Execute the input stage of the pipeline.""" # This implementation assumes the input stage has load_data and validate_data methods result = input_stage.load_data(self.input_source, self.input_source_type) if result["metadata"]["status"] != "error": if hasattr(self, "required_fields"): result = input_stage.validate_data(required_fields=self.required_fields) return result def _execute_stage(self, stage_instance, previous_result): """Execute a stage with the result from the previous stage.""" # Determine which stage we're executing based on the instance type if hasattr(stage_instance, "process"): # Processing stage result = stage_instance.process(previous_result) # Execute additional processing methods if configured if hasattr(self, "processing_config"): config = self.processing_config # Calculate statistics if configured if config.get("calculate_statistics"): result = stage_instance.calculate_statistics( numeric_fields=config.get("numeric_fields") ) # Apply filters if configured if "filters" in config: for filter_config in config["filters"]: result = stage_instance.filter_data( filter_config["filter_func"], filter_config.get("description") ) # Apply transformations if configured if "transformations" in config: result = stage_instance.transform_fields( config["transformations"], config.get("transformation_description") ) # Finalize the processing stage result = stage_instance.finalize() elif hasattr(stage_instance, "prepare"): # Output stage result = stage_instance.prepare(previous_result) # Execute additional output methods if configured if hasattr(self, "output_config"): config = self.output_config # Format as summary if configured if config.get("format_summary", False): result = stage_instance.format_as_summary() # Format as detailed report if configured if config.get("format_detailed", False): result = stage_instance.format_as_detailed_report() # Save to file if configured if "save_to_file" in config: for save_config in config["save_to_file"]: result = stage_instance.save_to_file( output_format=save_config.get("format", "json"), output_dir=save_config.get("dir", "./output"), filename=save_config.get("filename") ) # Print results if configured if config.get("print_results"): result = stage_instance.print_results( output_type=config.get("print_output_type", "summary") ) # Finalize the output stage result = stage_instance.finalize() else: # Unknown stage type raise ValueError(f"Unknown stage type: {type(stage_instance).__name__}") return result def configure_input(self, source, source_type="json", required_fields=None): """ Configure the input stage. Args: source: Path to the data file or raw data source_type: Type of data source (json, csv, raw) required_fields: List of required field names for validation """ self.input_source = source self.input_source_type = source_type if required_fields: self.required_fields = required_fields def configure_processing(self, config): """ Configure the processing stage. Args: config: Dictionary with processing configuration """ self.processing_config = config def configure_output(self, config): """ Configure the output stage. Args: config: Dictionary with output configuration """ self.output_config = config ================================================ FILE: codebase-architectures/pipeline-architecture/pipeline_manager/pipeline_manager.py ================================================ #!/usr/bin/env python3 """ Pipeline manager for the pipeline architecture. This module coordinates the execution of the pipeline stages. """ from datetime import datetime class PipelineManager: """Manager for coordinating pipeline stages.""" def __init__(self, name="Data Processing Pipeline"): """Initialize the pipeline manager.""" self.name = name self.stages = [] self.results = {} self.metadata = { "pipeline_name": name, "status": "initialized", "started_at": None, "completed_at": None, "errors": [] } def add_stage(self, stage_name, stage_instance): """ Add a stage to the pipeline. Args: stage_name: Name of the stage stage_instance: Instance of the stage class """ self.stages.append({ "name": stage_name, "instance": stage_instance, "status": "pending" }) def run(self): """ Run the pipeline by executing all stages in sequence. Returns: dict: Pipeline results including data and metadata """ self.metadata["started_at"] = datetime.now().isoformat() self.metadata["status"] = "running" print(f"\n=== Starting Pipeline: {self.name} ===") # Execute each stage for i, stage in enumerate(self.stages): stage_name = stage["name"] stage_instance = stage["instance"] print(f"\n--- Stage {i+1}: {stage_name} ---") try: # Execute the stage if i == 0: # First stage doesn't take input from previous stage result = self._execute_first_stage(stage_instance) else: # Pass result from previous stage previous_result = self.results[self.stages[i-1]["name"]] result = self._execute_stage(stage_instance, previous_result) # Store the result self.results[stage_name] = result # Update stage status stage["status"] = result["metadata"]["status"] # Check for errors if result["metadata"]["status"] in ["error", "skipped"]: print(f"Stage {stage_name} {result['metadata']['status']}") for error in result["metadata"].get("errors", []): print(f" Error: {error}") # Add errors to pipeline metadata self.metadata["errors"].append({ "stage": stage_name, "errors": result["metadata"].get("errors", []) }) else: print(f"Stage {stage_name} completed successfully") except Exception as e: # Handle unexpected errors error_message = f"Unexpected error in stage {stage_name}: {str(e)}" print(f" Error: {error_message}") # Update stage status stage["status"] = "error" # Add error to pipeline metadata self.metadata["errors"].append({ "stage": stage_name, "errors": [error_message] }) # Update pipeline status self.metadata["completed_at"] = datetime.now().isoformat() if self.metadata["errors"]: self.metadata["status"] = "completed_with_errors" else: self.metadata["status"] = "completed" # Calculate total execution time start_time = datetime.fromisoformat(self.metadata["started_at"]) end_time = datetime.fromisoformat(self.metadata["completed_at"]) execution_time = (end_time - start_time).total_seconds() self.metadata["execution_time_seconds"] = execution_time print(f"\n=== Pipeline {self.name} {self.metadata['status']} ===") print(f"Total execution time: {execution_time:.2f} seconds") return self._create_pipeline_result() def _execute_first_stage(self, stage_instance): """Execute the first stage of the pipeline.""" # This method should be overridden in subclasses to provide # specific implementation for the first stage raise NotImplementedError("Subclasses must implement _execute_first_stage") def _execute_stage(self, stage_instance, previous_result): """Execute a stage with the result from the previous stage.""" # This method should be overridden in subclasses to provide # specific implementation for subsequent stages raise NotImplementedError("Subclasses must implement _execute_stage") def get_final_result(self): """ Get the result from the final stage of the pipeline. Returns: dict: Result from the final stage """ if not self.stages: return None final_stage_name = self.stages[-1]["name"] if final_stage_name in self.results: return self.results[final_stage_name] return None def _create_pipeline_result(self): """Create a result dictionary for the entire pipeline.""" # Get the final result final_result = self.get_final_result() # Create pipeline result pipeline_result = { "metadata": self.metadata, "stages": [{ "name": stage["name"], "status": stage["status"] } for stage in self.stages] } # Add data from final stage if available if final_result and "data" in final_result: pipeline_result["data"] = final_result["data"] # Add analysis from final stage if available if final_result and "analysis" in final_result: pipeline_result["analysis"] = final_result["analysis"] return pipeline_result ================================================ FILE: codebase-architectures/pipeline-architecture/shared/utilities.py ================================================ #!/usr/bin/env python3 """ Shared utilities for the pipeline architecture. """ import json import csv import os from datetime import datetime def load_json_file(file_path): """Load data from a JSON file.""" try: with open(file_path, 'r') as file: return json.load(file) except FileNotFoundError: raise ValueError(f"File not found: {file_path}") except json.JSONDecodeError: raise ValueError(f"Invalid JSON format in file: {file_path}") def save_json_file(data, file_path): """Save data to a JSON file.""" directory = os.path.dirname(file_path) if directory and not os.path.exists(directory): os.makedirs(directory) with open(file_path, 'w') as file: json.dump(data, file, indent=2) def load_csv_file(file_path): """Load data from a CSV file.""" try: with open(file_path, 'r', newline='') as file: reader = csv.DictReader(file) return list(reader) except FileNotFoundError: raise ValueError(f"File not found: {file_path}") except Exception as e: raise ValueError(f"Error reading CSV file {file_path}: {str(e)}") def save_csv_file(data, file_path, fieldnames=None): """Save data to a CSV file.""" if not data: raise ValueError("No data to save") directory = os.path.dirname(file_path) if directory and not os.path.exists(directory): os.makedirs(directory) if fieldnames is None: fieldnames = data[0].keys() with open(file_path, 'w', newline='') as file: writer = csv.DictWriter(file, fieldnames=fieldnames) writer.writeheader() writer.writerows(data) def get_timestamp(): """Get the current timestamp.""" return datetime.now().isoformat() def validate_required_fields(data, required_fields): """Validate that all required fields are present in the data.""" if not isinstance(data, dict): raise ValueError("Data must be a dictionary") missing_fields = [field for field in required_fields if field not in data] if missing_fields: raise ValueError(f"Missing required fields: {', '.join(missing_fields)}") return True def format_currency(amount): """Format a number as currency.""" try: return f"${float(amount):.2f}" except (ValueError, TypeError): return "N/A" def format_percentage(value): """Format a number as percentage.""" try: return f"{float(value) * 100:.1f}%" except (ValueError, TypeError): return "N/A" def generate_report_filename(prefix="report", extension="json"): """Generate a filename for a report with timestamp.""" timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") return f"{prefix}_{timestamp}.{extension}" ================================================ FILE: codebase-architectures/pipeline-architecture/steps/input_stage.py ================================================ #!/usr/bin/env python3 """ Input stage for the pipeline architecture. This stage is responsible for loading and validating input data. """ import os import json from shared.utilities import load_json_file, load_csv_file, validate_required_fields class InputStage: """Input stage for data processing pipeline.""" def __init__(self): """Initialize the input stage.""" self.data = None self.metadata = { "stage": "input", "status": "initialized", "errors": [] } def load_data(self, source, source_type="json"): """ Load data from the specified source. Args: source: Path to the data file or raw data source_type: Type of data source (json, csv, raw) Returns: dict: Stage result with data and metadata """ try: self.metadata["source"] = source self.metadata["source_type"] = source_type # Load data based on source type if source_type == "json": if isinstance(source, str) and os.path.exists(source): self.data = load_json_file(source) elif isinstance(source, str): self.data = json.loads(source) else: self.data = source elif source_type == "csv": self.data = load_csv_file(source) elif source_type == "raw": self.data = source else: raise ValueError(f"Unsupported source type: {source_type}") self.metadata["status"] = "data_loaded" self.metadata["record_count"] = len(self.data) if isinstance(self.data, list) else 1 return self._create_result() except Exception as e: self.metadata["status"] = "error" self.metadata["errors"].append(str(e)) return self._create_result() def validate_data(self, schema=None, required_fields=None): """ Validate the loaded data against a schema or required fields. Args: schema: Schema definition for validation required_fields: List of required field names Returns: dict: Stage result with data and metadata """ if self.data is None: self.metadata["status"] = "error" self.metadata["errors"].append("No data loaded to validate") return self._create_result() try: validation_errors = [] # Validate required fields if specified if required_fields: if isinstance(self.data, list): for i, item in enumerate(self.data): try: validate_required_fields(item, required_fields) except ValueError as e: validation_errors.append(f"Record {i}: {str(e)}") else: try: validate_required_fields(self.data, required_fields) except ValueError as e: validation_errors.append(str(e)) # Update metadata based on validation results if validation_errors: self.metadata["status"] = "validation_failed" self.metadata["errors"].extend(validation_errors) else: self.metadata["status"] = "validated" return self._create_result() except Exception as e: self.metadata["status"] = "error" self.metadata["errors"].append(f"Validation error: {str(e)}") return self._create_result() def transform_data(self, transform_func): """ Apply a transformation function to the data. Args: transform_func: Function to transform the data Returns: dict: Stage result with data and metadata """ if self.data is None: self.metadata["status"] = "error" self.metadata["errors"].append("No data loaded to transform") return self._create_result() try: self.data = transform_func(self.data) self.metadata["status"] = "transformed" return self._create_result() except Exception as e: self.metadata["status"] = "error" self.metadata["errors"].append(f"Transformation error: {str(e)}") return self._create_result() def _create_result(self): """Create a result dictionary with data and metadata.""" return { "data": self.data, "metadata": self.metadata } ================================================ FILE: codebase-architectures/pipeline-architecture/steps/output_stage.py ================================================ #!/usr/bin/env python3 """ Output stage for the pipeline architecture. This stage is responsible for formatting and delivering the final results. """ import os import json from datetime import datetime from shared.utilities import save_json_file, save_csv_file, generate_report_filename class OutputStage: """Output stage for formatting and delivering results.""" def __init__(self): """Initialize the output stage.""" self.data = None self.analysis = None self.metadata = { "stage": "output", "status": "initialized", "errors": [], "output_formats": [] } def prepare(self, processing_result): """ Prepare the output stage with data from the processing stage. Args: processing_result: Result from the processing stage Returns: dict: Stage result with data and metadata """ # Check if processing stage had errors if processing_result["metadata"]["status"] in ["error", "skipped"]: self.metadata["status"] = "skipped" self.metadata["errors"].append("Processing stage had errors, output skipped") return self._create_result() # Get data and metadata from processing stage self.data = processing_result["data"] self.metadata["input_metadata"] = processing_result["metadata"]["input_metadata"] self.metadata["processing_metadata"] = processing_result["metadata"] # Get analysis if available if "analysis" in processing_result: self.analysis = processing_result["analysis"] # Initialize output self.metadata["status"] = "preparing" self.metadata["started_at"] = datetime.now().isoformat() return self._create_result() def format_as_summary(self): """ Format the data as a summary report. Returns: dict: Stage result with data and metadata """ if self.data is None: self.metadata["status"] = "error" self.metadata["errors"].append("No data to format") return self._create_result() try: # Create summary summary = { "report_type": "summary", "generated_at": datetime.now().isoformat(), "data_source": self.metadata.get("input_metadata", {}).get("source", "unknown"), "record_count": len(self.data) if isinstance(self.data, list) else 1 } # Add statistics if available if self.analysis and "statistics" in self.analysis: summary["statistics"] = self.analysis["statistics"] # Add processing information if "processing_metadata" in self.metadata: processing_meta = self.metadata["processing_metadata"] if "processing_steps" in processing_meta: summary["processing_steps"] = processing_meta["processing_steps"] if "processing_time_seconds" in processing_meta: summary["processing_time_seconds"] = processing_meta["processing_time_seconds"] # Store the summary self.summary = summary # Update metadata self.metadata["output_formats"].append("summary") return self._create_result() except Exception as e: self.metadata["status"] = "error" self.metadata["errors"].append(f"Summary formatting error: {str(e)}") return self._create_result() def format_as_detailed_report(self): """ Format the data as a detailed report. Returns: dict: Stage result with data and metadata """ if self.data is None: self.metadata["status"] = "error" self.metadata["errors"].append("No data to format") return self._create_result() try: # Create detailed report report = { "report_type": "detailed", "generated_at": datetime.now().isoformat(), "data_source": self.metadata.get("input_metadata", {}).get("source", "unknown"), "record_count": len(self.data) if isinstance(self.data, list) else 1, "data": self.data } # Add analysis if available if self.analysis: report["analysis"] = self.analysis # Add processing information if "processing_metadata" in self.metadata: report["processing_info"] = { "steps": self.metadata["processing_metadata"].get("processing_steps", []), "filters": self.metadata["processing_metadata"].get("filters_applied", []), "transformations": self.metadata["processing_metadata"].get("transformations_applied", []), "processing_time_seconds": self.metadata["processing_metadata"].get("processing_time_seconds") } # Store the detailed report self.detailed_report = report # Update metadata self.metadata["output_formats"].append("detailed_report") return self._create_result() except Exception as e: self.metadata["status"] = "error" self.metadata["errors"].append(f"Detailed report formatting error: {str(e)}") return self._create_result() def save_to_file(self, output_format="json", output_dir="./output", filename=None): """ Save the formatted output to a file. Args: output_format: Format to save (json, csv) output_dir: Directory to save the file filename: Optional filename (generated if not provided) Returns: dict: Stage result with data and metadata """ try: # Create output directory if it doesn't exist if not os.path.exists(output_dir): os.makedirs(output_dir) # Determine what to save if output_format == "json": if hasattr(self, "detailed_report"): data_to_save = self.detailed_report file_prefix = "detailed_report" elif hasattr(self, "summary"): data_to_save = self.summary file_prefix = "summary_report" else: data_to_save = { "data": self.data, "generated_at": datetime.now().isoformat() } file_prefix = "data_export" # Generate filename if not provided if not filename: filename = generate_report_filename(file_prefix, "json") # Save to file file_path = os.path.join(output_dir, filename) save_json_file(data_to_save, file_path) elif output_format == "csv": # CSV format only works for list data if not isinstance(self.data, list): raise ValueError("CSV output format requires list data") # Generate filename if not provided if not filename: filename = generate_report_filename("data_export", "csv") # Save to file file_path = os.path.join(output_dir, filename) save_csv_file(self.data, file_path) else: raise ValueError(f"Unsupported output format: {output_format}") # Update metadata self.metadata["output_files"] = self.metadata.get("output_files", []) self.metadata["output_files"].append({ "format": output_format, "path": file_path, "filename": filename }) return self._create_result() except Exception as e: self.metadata["status"] = "error" self.metadata["errors"].append(f"File save error: {str(e)}") return self._create_result() def print_results(self, output_type="summary"): """ Print the results to the console. Args: output_type: Type of output to print (summary, detailed) Returns: dict: Stage result with data and metadata """ try: if output_type == "summary" and hasattr(self, "summary"): print("\n===== SUMMARY REPORT =====") print(f"Generated at: {self.summary['generated_at']}") print(f"Data source: {self.summary['data_source']}") print(f"Record count: {self.summary['record_count']}") if "statistics" in self.summary: print("\n----- Statistics -----") for field, stats in self.summary["statistics"].items(): print(f"\n{field}:") for stat_name, stat_value in stats.items(): print(f" {stat_name}: {stat_value}") if "processing_steps" in self.summary: print("\n----- Processing Steps -----") for step in self.summary["processing_steps"]: print(f"- {step}") elif output_type == "detailed" and hasattr(self, "detailed_report"): print("\n===== DETAILED REPORT =====") print(f"Generated at: {self.detailed_report['generated_at']}") print(f"Data source: {self.detailed_report['data_source']}") print(f"Record count: {self.detailed_report['record_count']}") if "analysis" in self.detailed_report: print("\n----- Analysis -----") for analysis_type, analysis_data in self.detailed_report["analysis"].items(): print(f"\n{analysis_type}:") print(json.dumps(analysis_data, indent=2)) print("\n----- Data Sample -----") if isinstance(self.data, list): sample_size = min(3, len(self.data)) for i in range(sample_size): print(f"\nRecord {i+1}:") print(json.dumps(self.data[i], indent=2)) else: print(json.dumps(self.data, indent=2)) else: print("\n===== DATA OUTPUT =====") if isinstance(self.data, list): print(f"Record count: {len(self.data)}") sample_size = min(3, len(self.data)) print(f"\nShowing {sample_size} sample records:") for i in range(sample_size): print(f"\nRecord {i+1}:") print(json.dumps(self.data[i], indent=2)) else: print(json.dumps(self.data, indent=2)) # Update metadata self.metadata["output_formats"].append("console") return self._create_result() except Exception as e: self.metadata["status"] = "error" self.metadata["errors"].append(f"Print error: {str(e)}") return self._create_result() def finalize(self): """ Finalize the output stage. Returns: dict: Stage result with data and metadata """ if self.metadata["status"] not in ["error", "skipped"]: self.metadata["status"] = "completed" self.metadata["completed_at"] = datetime.now().isoformat() # Calculate processing time if we have start time if "started_at" in self.metadata: start_time = datetime.fromisoformat(self.metadata["started_at"]) end_time = datetime.fromisoformat(self.metadata["completed_at"]) processing_time = (end_time - start_time).total_seconds() self.metadata["processing_time_seconds"] = processing_time return self._create_result() def _create_result(self): """Create a result dictionary with data and metadata.""" result = { "data": self.data, "metadata": self.metadata } # Add analysis if available if self.analysis: result["analysis"] = self.analysis # Add formatted outputs if available if hasattr(self, "summary"): result["summary"] = self.summary if hasattr(self, "detailed_report"): result["detailed_report"] = self.detailed_report return result ================================================ FILE: codebase-architectures/pipeline-architecture/steps/processing_stage.py ================================================ #!/usr/bin/env python3 """ Processing stage for the pipeline architecture. This stage is responsible for transforming and analyzing the data. """ import statistics from datetime import datetime class ProcessingStage: """Processing stage for data transformation and analysis.""" def __init__(self): """Initialize the processing stage.""" self.data = None self.metadata = { "stage": "processing", "status": "initialized", "errors": [], "processing_steps": [] } def process(self, input_result): """ Process the data from the input stage. Args: input_result: Result from the input stage Returns: dict: Stage result with processed data and metadata """ # Check if input stage had errors if input_result["metadata"]["status"] in ["error", "validation_failed"]: self.metadata["status"] = "skipped" self.metadata["errors"].append("Input stage had errors, processing skipped") return self._create_result() # Get data from input stage self.data = input_result["data"] self.metadata["input_metadata"] = input_result["metadata"] # Initialize processing self.metadata["status"] = "processing" self.metadata["started_at"] = datetime.now().isoformat() return self._create_result() def calculate_statistics(self, numeric_fields=None): """ Calculate statistics for numeric fields in the data. Args: numeric_fields: List of field names to calculate statistics for Returns: dict: Stage result with data and metadata """ if self.data is None: self.metadata["status"] = "error" self.metadata["errors"].append("No data to process") return self._create_result() try: # Determine fields to analyze if numeric_fields is None: # Try to automatically detect numeric fields if isinstance(self.data, list) and len(self.data) > 0: sample = self.data[0] numeric_fields = [ field for field, value in sample.items() if isinstance(value, (int, float)) or ( isinstance(value, str) and value.replace('.', '', 1).isdigit() ) ] # Calculate statistics stats = {} if isinstance(self.data, list) and numeric_fields: for field in numeric_fields: try: # Extract numeric values values = [] for item in self.data: if field in item: value = item[field] if isinstance(value, (int, float)): values.append(value) elif isinstance(value, str) and value.replace('.', '', 1).isdigit(): values.append(float(value)) # Calculate statistics if we have values if values: field_stats = { "count": len(values), "min": min(values), "max": max(values), "sum": sum(values), "mean": statistics.mean(values), "median": statistics.median(values) } # Add standard deviation if we have enough values if len(values) > 1: field_stats["std_dev"] = statistics.stdev(values) stats[field] = field_stats except Exception as e: self.metadata["errors"].append(f"Error calculating statistics for field '{field}': {str(e)}") # Add statistics to data if not hasattr(self, "analysis"): self.analysis = {} self.analysis["statistics"] = stats # Update metadata self.metadata["processing_steps"].append("calculate_statistics") self.metadata["statistics_fields"] = list(stats.keys()) return self._create_result() except Exception as e: self.metadata["status"] = "error" self.metadata["errors"].append(f"Statistics calculation error: {str(e)}") return self._create_result() def filter_data(self, filter_func, description=None): """ Filter the data using the provided filter function. Args: filter_func: Function that takes a data item and returns True to keep it description: Description of the filter for metadata Returns: dict: Stage result with data and metadata """ if self.data is None: self.metadata["status"] = "error" self.metadata["errors"].append("No data to filter") return self._create_result() try: original_count = len(self.data) if isinstance(self.data, list) else 1 # Apply filter if isinstance(self.data, list): self.data = [item for item in self.data if filter_func(item)] else: self.data = self.data if filter_func(self.data) else None # Update metadata filtered_count = len(self.data) if isinstance(self.data, list) else (1 if self.data else 0) filter_info = { "description": description or "Custom filter", "original_count": original_count, "filtered_count": filtered_count, "removed_count": original_count - filtered_count } if not hasattr(self, "filters_applied"): self.filters_applied = [] self.filters_applied.append(filter_info) self.metadata["processing_steps"].append("filter_data") self.metadata["filters_applied"] = self.filters_applied return self._create_result() except Exception as e: self.metadata["status"] = "error" self.metadata["errors"].append(f"Filter error: {str(e)}") return self._create_result() def transform_fields(self, transformations, description=None): """ Apply transformations to specific fields in the data. Args: transformations: Dict mapping field names to transformation functions description: Description of the transformations for metadata Returns: dict: Stage result with data and metadata """ if self.data is None: self.metadata["status"] = "error" self.metadata["errors"].append("No data to transform") return self._create_result() try: # Apply transformations if isinstance(self.data, list): for item in self.data: for field, transform_func in transformations.items(): if field in item: item[field] = transform_func(item[field]) else: for field, transform_func in transformations.items(): if field in self.data: self.data[field] = transform_func(self.data[field]) # Update metadata transform_info = { "description": description or "Field transformations", "fields_transformed": list(transformations.keys()) } if not hasattr(self, "transformations_applied"): self.transformations_applied = [] self.transformations_applied.append(transform_info) self.metadata["processing_steps"].append("transform_fields") self.metadata["transformations_applied"] = self.transformations_applied return self._create_result() except Exception as e: self.metadata["status"] = "error" self.metadata["errors"].append(f"Transformation error: {str(e)}") return self._create_result() def finalize(self): """ Finalize the processing stage. Returns: dict: Stage result with data and metadata """ if self.metadata["status"] not in ["error", "skipped"]: self.metadata["status"] = "completed" self.metadata["completed_at"] = datetime.now().isoformat() # Calculate processing time if we have start time if "started_at" in self.metadata: start_time = datetime.fromisoformat(self.metadata["started_at"]) end_time = datetime.fromisoformat(self.metadata["completed_at"]) processing_time = (end_time - start_time).total_seconds() self.metadata["processing_time_seconds"] = processing_time return self._create_result() def _create_result(self): """Create a result dictionary with data and metadata.""" result = { "data": self.data, "metadata": self.metadata } # Add analysis if available if hasattr(self, "analysis"): result["analysis"] = self.analysis return result ================================================ FILE: codebase-architectures/vertical-slice-architecture/README.md ================================================ # Vertical Slice Architecture This directory demonstrates a Vertical Slice Architecture implementation with a simple task management application. ## Structure ``` vertical-slice-architecture/ ├── features/ │ ├── tasks/ │ │ ├── api.py # Feature-specific API endpoints │ │ ├── service.py # Core business logic │ │ ├── model.py # Data models/schema │ │ └── README.md # Feature documentation │ └── users/ │ ├── api.py # Feature-specific API endpoints │ ├── service.py # Core business logic │ ├── model.py # Data models/schema │ └── README.md # Feature documentation ├── shared/ │ ├── utils.py # Shared utilities │ └── db.py # Shared database connections └── main.py # Application entry point ``` ## Benefits - Excellent feature isolation; clear and consistent structure - Each feature is independently testable and maintainable - Clear feature-level documentation enhances comprehension ## Cons - Potential for duplicated logic across features - Complexity increases when blending features; shared logic must be explicitly managed ## Running the Example ```bash uv run main.py ``` ================================================ FILE: codebase-architectures/vertical-slice-architecture/features/projects/README.md ================================================ # Projects Feature This feature provides functionality for managing projects in the task management system. ## Components - **model.py**: Defines the Project model with fields like name, description, user_id, and task_ids - **service.py**: Contains business logic for project management - **api.py**: Provides API endpoints for project operations ## Functionality - Create, read, update, and delete projects - Assign tasks to projects - Remove tasks from projects - Get all tasks for a specific project - Get all projects for a specific user ## Relationships - Projects are owned by users (one-to-many) - Projects can contain multiple tasks (one-to-many) ================================================ FILE: codebase-architectures/vertical-slice-architecture/features/projects/api.py ================================================ #!/usr/bin/env python3 """ Project API endpoints. """ from .service import ProjectService from features.tasks.service import TaskService class ProjectAPI: """API endpoints for project management.""" @staticmethod def create_project(name, description=None, user_id=None): """Create a new project.""" project_data = { "name": name, "description": description, "user_id": user_id } return ProjectService.create_project(project_data) @staticmethod def get_project(project_id): """Get a project by ID.""" project = ProjectService.get_project(project_id) if not project: return {"error": f"Project with ID {project_id} not found"} return project @staticmethod def get_all_projects(): """Get all projects.""" return ProjectService.get_all_projects() @staticmethod def get_user_projects(user_id): """Get all projects for a specific user.""" return ProjectService.get_user_projects(user_id) @staticmethod def update_project(project_id, project_data): """Update a project.""" project = ProjectService.update_project(project_id, project_data) if not project: return {"error": f"Project with ID {project_id} not found"} return project @staticmethod def delete_project(project_id): """Delete a project.""" success = ProjectService.delete_project(project_id) if not success: return {"error": f"Project with ID {project_id} not found"} return {"message": f"Project with ID {project_id} deleted successfully"} @staticmethod def add_task_to_project(project_id, task_id): """Add a task to a project.""" success = ProjectService.add_task_to_project(project_id, task_id) if not success: return {"error": "Project or task not found"} return {"message": f"Task added to project successfully"} @staticmethod def remove_task_from_project(project_id, task_id): """Remove a task from a project.""" success = ProjectService.remove_task_from_project(project_id, task_id) if not success: return {"error": "Project or task not found, or task is not in project"} return {"message": f"Task removed from project successfully"} @staticmethod def get_project_tasks(project_id): """Get all tasks for a specific project.""" project = ProjectService.get_project(project_id) if not project: return {"error": f"Project with ID {project_id} not found"} tasks = ProjectService.get_project_tasks(project_id) return tasks ================================================ FILE: codebase-architectures/vertical-slice-architecture/features/projects/model.py ================================================ #!/usr/bin/env python3 """ Project model definition. """ from shared.utils import generate_id, get_timestamp class Project: """Project model representing a collection of tasks.""" def __init__(self, name, description=None, user_id=None, id=None): self.id = id or generate_id() self.name = name self.description = description self.user_id = user_id # Owner of the project self.task_ids = [] # List of task IDs associated with this project self.created_at = get_timestamp() self.updated_at = self.created_at def to_dict(self): """Convert project to dictionary.""" return { "id": self.id, "name": self.name, "description": self.description, "user_id": self.user_id, "task_ids": self.task_ids, "created_at": self.created_at, "updated_at": self.updated_at } @classmethod def from_dict(cls, data): """Create a project from dictionary.""" project = cls( name=data["name"], description=data.get("description"), user_id=data.get("user_id"), id=data.get("id") ) project.task_ids = data.get("task_ids", []) project.created_at = data.get("created_at", project.created_at) project.updated_at = data.get("updated_at", project.updated_at) return project ================================================ FILE: codebase-architectures/vertical-slice-architecture/features/projects/service.py ================================================ #!/usr/bin/env python3 """ Project service containing business logic for project management. """ from shared.db import db from shared.utils import validate_required_fields, get_timestamp from .model import Project class ProjectService: """Service for managing projects.""" @staticmethod def create_project(project_data): """Create a new project.""" validate_required_fields(project_data, ["name"]) project = Project(**project_data) db.insert("projects", project.id, project.to_dict()) return project.to_dict() @staticmethod def get_project(project_id): """Get a project by ID.""" project_data = db.get("projects", project_id) if not project_data: return None return project_data @staticmethod def get_all_projects(): """Get all projects.""" return db.get_all("projects") @staticmethod def get_user_projects(user_id): """Get all projects for a specific user.""" all_projects = db.get_all("projects") return [project for project in all_projects if project.get("user_id") == user_id] @staticmethod def update_project(project_id, project_data): """Update a project.""" existing_project = db.get("projects", project_id) if not existing_project: return None # Update fields for key, value in project_data.items(): if key not in ["id", "created_at"]: existing_project[key] = value # Update timestamp existing_project["updated_at"] = get_timestamp() # Save to database db.update("projects", project_id, existing_project) return existing_project @staticmethod def delete_project(project_id): """Delete a project.""" return db.delete("projects", project_id) @staticmethod def add_task_to_project(project_id, task_id): """Add a task to a project.""" project = db.get("projects", project_id) if not project: return False # Check if task exists task = db.get("tasks", task_id) if not task: return False # Add task to project if not already added if task_id not in project["task_ids"]: project["task_ids"].append(task_id) project["updated_at"] = get_timestamp() db.update("projects", project_id, project) return True @staticmethod def remove_task_from_project(project_id, task_id): """Remove a task from a project.""" project = db.get("projects", project_id) if not project: return False # Remove task from project if it exists if task_id in project["task_ids"]: project["task_ids"].remove(task_id) project["updated_at"] = get_timestamp() db.update("projects", project_id, project) return True return False @staticmethod def get_project_tasks(project_id): """Get all tasks for a specific project.""" project = db.get("projects", project_id) if not project: return [] tasks = [] for task_id in project["task_ids"]: task = db.get("tasks", task_id) if task: tasks.append(task) return tasks ================================================ FILE: codebase-architectures/vertical-slice-architecture/features/tasks/README.md ================================================ # Tasks Feature This feature handles the management of tasks in the application. ## Components - **model.py**: Defines the Task data model - **service.py**: Contains business logic for task operations - **api.py**: Exposes task management endpoints ## Functionality - Create, read, update, and delete tasks - Assign tasks to users - Filter tasks by user - Track task status and timestamps ================================================ FILE: codebase-architectures/vertical-slice-architecture/features/tasks/api.py ================================================ #!/usr/bin/env python3 """ Task API endpoints. """ from .service import TaskService class TaskAPI: """API endpoints for task management.""" @staticmethod def create_task(title, description=None, user_id=None): """Create a new task.""" task_data = { "title": title, "description": description, "user_id": user_id } return TaskService.create_task(task_data) @staticmethod def get_task(task_id): """Get a task by ID.""" task = TaskService.get_task(task_id) if not task: return {"error": f"Task with ID {task_id} not found"} return task @staticmethod def get_all_tasks(): """Get all tasks.""" return TaskService.get_all_tasks() @staticmethod def get_user_tasks(user_id): """Get all tasks for a specific user.""" return TaskService.get_user_tasks(user_id) @staticmethod def update_task(task_id, task_data): """Update a task.""" task = TaskService.update_task(task_id, task_data) if not task: return {"error": f"Task with ID {task_id} not found"} return task @staticmethod def delete_task(task_id): """Delete a task.""" success = TaskService.delete_task(task_id) if not success: return {"error": f"Task with ID {task_id} not found"} return {"message": f"Task with ID {task_id} deleted successfully"} ================================================ FILE: codebase-architectures/vertical-slice-architecture/features/tasks/model.py ================================================ #!/usr/bin/env python3 """ Task model definition. """ from shared.utils import generate_id, get_timestamp class Task: """Task model representing a to-do item.""" def __init__(self, title, description=None, user_id=None, status="pending", id=None): self.id = id or generate_id() self.title = title self.description = description self.user_id = user_id self.status = status self.created_at = get_timestamp() self.updated_at = self.created_at def to_dict(self): """Convert task to dictionary.""" return { "id": self.id, "title": self.title, "description": self.description, "user_id": self.user_id, "status": self.status, "created_at": self.created_at, "updated_at": self.updated_at } @classmethod def from_dict(cls, data): """Create a task from dictionary.""" task = cls( title=data["title"], description=data.get("description"), user_id=data.get("user_id"), status=data.get("status", "pending"), id=data.get("id") ) task.created_at = data.get("created_at", task.created_at) task.updated_at = data.get("updated_at", task.updated_at) return task ================================================ FILE: codebase-architectures/vertical-slice-architecture/features/tasks/service.py ================================================ #!/usr/bin/env python3 """ Task service containing business logic for task management. """ from shared.db import db from shared.utils import validate_required_fields, get_timestamp from .model import Task class TaskService: """Service for managing tasks.""" @staticmethod def create_task(task_data): """Create a new task.""" validate_required_fields(task_data, ["title"]) task = Task(**task_data) db.insert("tasks", task.id, task.to_dict()) return task.to_dict() @staticmethod def get_task(task_id): """Get a task by ID.""" task_data = db.get("tasks", task_id) if not task_data: return None return task_data @staticmethod def get_all_tasks(): """Get all tasks.""" return db.get_all("tasks") @staticmethod def get_user_tasks(user_id): """Get all tasks for a specific user.""" all_tasks = db.get_all("tasks") return [task for task in all_tasks if task.get("user_id") == user_id] @staticmethod def update_task(task_id, task_data): """Update a task.""" existing_task = db.get("tasks", task_id) if not existing_task: return None # Update fields for key, value in task_data.items(): if key not in ["id", "created_at"]: existing_task[key] = value # Update timestamp existing_task["updated_at"] = get_timestamp() # Save to database db.update("tasks", task_id, existing_task) return existing_task @staticmethod def delete_task(task_id): """Delete a task.""" return db.delete("tasks", task_id) ================================================ FILE: codebase-architectures/vertical-slice-architecture/features/users/README.md ================================================ # Users Feature This feature handles user management in the application. ## Components - **model.py**: Defines the User data model - **service.py**: Contains business logic for user operations - **api.py**: Exposes user management endpoints ## Functionality - Create, read, update, and delete users - Validate unique usernames - Retrieve users by ID or username ================================================ FILE: codebase-architectures/vertical-slice-architecture/features/users/api.py ================================================ #!/usr/bin/env python3 """ User API endpoints. """ from .service import UserService class UserAPI: """API endpoints for user management.""" @staticmethod def create_user(username, email, name=None): """Create a new user.""" try: user_data = { "username": username, "email": email, "name": name } return UserService.create_user(user_data) except ValueError as e: return {"error": str(e)} @staticmethod def get_user(user_id): """Get a user by ID.""" user = UserService.get_user(user_id) if not user: return {"error": f"User with ID {user_id} not found"} return user @staticmethod def get_by_username(username): """Get a user by username.""" user = UserService.get_by_username(username) if not user: return {"error": f"User with username '{username}' not found"} return user @staticmethod def get_all_users(): """Get all users.""" return UserService.get_all_users() @staticmethod def update_user(user_id, user_data): """Update a user.""" try: user = UserService.update_user(user_id, user_data) if not user: return {"error": f"User with ID {user_id} not found"} return user except ValueError as e: return {"error": str(e)} @staticmethod def delete_user(user_id): """Delete a user.""" success = UserService.delete_user(user_id) if not success: return {"error": f"User with ID {user_id} not found"} return {"message": f"User with ID {user_id} deleted successfully"} ================================================ FILE: codebase-architectures/vertical-slice-architecture/features/users/model.py ================================================ #!/usr/bin/env python3 """ User model definition. """ from shared.utils import generate_id, get_timestamp class User: """User model representing an application user.""" def __init__(self, username, email, name=None, id=None): self.id = id or generate_id() self.username = username self.email = email self.name = name self.created_at = get_timestamp() self.updated_at = self.created_at def to_dict(self): """Convert user to dictionary.""" return { "id": self.id, "username": self.username, "email": self.email, "name": self.name, "created_at": self.created_at, "updated_at": self.updated_at } @classmethod def from_dict(cls, data): """Create a user from dictionary.""" user = cls( username=data["username"], email=data["email"], name=data.get("name"), id=data.get("id") ) user.created_at = data.get("created_at", user.created_at) user.updated_at = data.get("updated_at", user.updated_at) return user ================================================ FILE: codebase-architectures/vertical-slice-architecture/features/users/service.py ================================================ #!/usr/bin/env python3 """ User service containing business logic for user management. """ from shared.db import db from shared.utils import validate_required_fields, get_timestamp from .model import User class UserService: """Service for managing users.""" @staticmethod def create_user(user_data): """Create a new user.""" validate_required_fields(user_data, ["username", "email"]) # Check if username already exists all_users = db.get_all("users") if any(user["username"] == user_data["username"] for user in all_users): raise ValueError(f"Username '{user_data['username']}' already exists") user = User(**user_data) db.insert("users", user.id, user.to_dict()) return user.to_dict() @staticmethod def get_user(user_id): """Get a user by ID.""" user_data = db.get("users", user_id) if not user_data: return None return user_data @staticmethod def get_by_username(username): """Get a user by username.""" all_users = db.get_all("users") for user in all_users: if user["username"] == username: return user return None @staticmethod def get_all_users(): """Get all users.""" return db.get_all("users") @staticmethod def update_user(user_id, user_data): """Update a user.""" existing_user = db.get("users", user_id) if not existing_user: return None # Check if username is being changed and already exists if "username" in user_data and user_data["username"] != existing_user["username"]: all_users = db.get_all("users") if any(user["username"] == user_data["username"] for user in all_users if user["id"] != user_id): raise ValueError(f"Username '{user_data['username']}' already exists") # Update fields for key, value in user_data.items(): if key not in ["id", "created_at"]: existing_user[key] = value # Update timestamp existing_user["updated_at"] = get_timestamp() # Save to database db.update("users", user_id, existing_user) return existing_user @staticmethod def delete_user(user_id): """Delete a user.""" return db.delete("users", user_id) ================================================ FILE: codebase-architectures/vertical-slice-architecture/main.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # ] # /// """ Main application entry point for the Vertical Slice Architecture example. """ from features.users.api import UserAPI from features.tasks.api import TaskAPI from features.projects.api import ProjectAPI def display_header(text): """Display a header with the given text.""" print("\n" + "=" * 50) print(f" {text}") print("=" * 50) def display_result(result): """Display a result.""" if isinstance(result, list): for item in result: print(f"- {item}") elif isinstance(result, dict): for key, value in result.items(): print(f"{key}: {value}") else: print(result) def main(): """Run the application.""" display_header("Vertical Slice Architecture Example") # Create users display_header("Creating Users") user1 = UserAPI.create_user("johndoe", "john@example.com", "John Doe") display_result(user1) user2 = UserAPI.create_user("janedoe", "jane@example.com", "Jane Doe") display_result(user2) # Try to create a user with an existing username duplicate_user = UserAPI.create_user("johndoe", "another@example.com") display_result(duplicate_user) # Get all users display_header("All Users") all_users = UserAPI.get_all_users() for user in all_users: display_result(user) # Create tasks display_header("Creating Tasks") task1 = TaskAPI.create_task("Complete project", "Finish the architecture example", user1["id"]) display_result(task1) task2 = TaskAPI.create_task("Review code", "Check for bugs and improvements", user2["id"]) display_result(task2) task3 = TaskAPI.create_task("Write documentation", "Document the architecture", user1["id"]) display_result(task3) # Get user tasks display_header(f"Tasks for {user1['name']}") user1_tasks = TaskAPI.get_user_tasks(user1["id"]) for task in user1_tasks: display_result(task) # Update a task display_header("Updating a Task") updated_task = TaskAPI.update_task(task1["id"], {"status": "completed"}) display_result(updated_task) # Delete a task display_header("Deleting a Task") delete_result = TaskAPI.delete_task(task2["id"]) display_result(delete_result) # Get all remaining tasks display_header("All Remaining Tasks") all_tasks = TaskAPI.get_all_tasks() for task in all_tasks: display_result(task) # Create a project display_header("Creating a Project") project = ProjectAPI.create_project("Task Management System", "A project for managing tasks", user1["id"]) display_result(project) # Add tasks to the project display_header("Adding Tasks to Project") add_task1 = ProjectAPI.add_task_to_project(project["id"], task1["id"]) display_result(add_task1) add_task3 = ProjectAPI.add_task_to_project(project["id"], task3["id"]) display_result(add_task3) # Get project tasks display_header(f"Tasks in Project: {project['name']}") project_tasks = ProjectAPI.get_project_tasks(project["id"]) for task in project_tasks: display_result(task) # Get user projects display_header(f"Projects for {user1['name']}") user_projects = ProjectAPI.get_user_projects(user1["id"]) for proj in user_projects: display_result(proj) if __name__ == "__main__": main() ================================================ FILE: data/analytics.csv ================================================ id,name,age,city,score,is_active,status,created_at 94efbf8b-4c95-4feb-9eda-900192276be7,Fiona,33,Singapore,95.48,True,active,2024-04-30 efcbb1f5-ffaf-4b40-a44b-cd67f6508eec,Alice,46,Paris,37.81,False,active,2023-10-31 9c2378d3-f46d-4f19-8e9e-b9053e6e57ea,Charlie,54,Tokyo,86.24,False,archived,2023-11-15 12a6dc88-1bd5-4a20-b729-e7a70918a60b,Charlie,31,Tokyo,61.24,True,pending,2024-03-26 dc1cf50e-c3c2-4843-8137-131834f7b00a,Jane,26,London,22.61,True,inactive,2024-11-04 db0ac268-3666-4a03-a284-e35380ae8c84,Jane,34,New York,3.1,True,archived,2024-01-29 24ab09dd-163a-4faf-8e06-d3fb6418dec5,Alice,64,Tokyo,6.17,True,pending,2024-03-08 27f8624d-c41a-4569-bbbb-1348b94fa0fd,Jane,34,Paris,43.44,True,active,2023-04-19 1666fe7c-a2b1-44d5-abba-293c4e0f9b23,Charlie,58,Sydney,85.02,True,active,2024-09-09 66036da1-7704-4ca6-9ec2-1e6d560db610,Alice,61,Singapore,93.69,True,archived,2024-05-06 04fa9318-9e6d-4029-be2d-9bc7ecf37f3f,John,24,Tokyo,14.59,True,inactive,2023-08-17 4586fbe1-acf9-4bfa-8237-ab697cdc69ed,Diana,43,Sydney,60.84,True,archived,2023-11-06 20b827b3-f183-44db-9f4f-f30b361c8a83,Diana,63,Singapore,96.18,False,inactive,2024-08-02 779dbbe0-b509-4332-98ee-d89887f48cec,Bob,47,Toronto,37.85,False,active,2025-01-25 8612b8d7-4524-4575-a782-a01c4a0c88a3,Bob,25,Berlin,88.28,False,pending,2025-01-16 ad992026-c339-40eb-a669-f16983d226ca,Diana,29,Berlin,84.17,False,active,2023-07-26 b3ef272f-3fc9-4422-88db-ed39858f1f68,John,20,New York,78.33,True,archived,2023-04-16 2a0ace4c-ee9e-4b6b-9005-897210109cca,Bob,49,Toronto,56.91,False,active,2023-07-10 3674700e-a83f-4450-97c8-04f80f2d2e89,Charlie,30,Sydney,80.83,False,active,2024-10-22 a17712d7-10f1-48e3-b1fd-8c65afb0442a,Jane,40,Toronto,35.85,False,inactive,2024-02-03 3b1e3e89-9ccb-46cf-9954-cb0903e8e02d,Diana,48,Singapore,37.58,True,inactive,2023-06-13 a4461690-a009-48c1-96d2-ed89dd1907a7,Diana,21,Tokyo,32.35,False,pending,2024-06-03 7157129c-0b3d-4040-8609-65afe4322ed2,Alice,65,New York,40.59,False,inactive,2024-02-19 dc3c472f-d16b-47e2-95c4-61c461e2b228,Diana,25,Singapore,48.32,False,inactive,2024-10-03 34e6e614-f376-4c1c-b3b5-f2926987115f,Bob,30,Tokyo,72.08,False,inactive,2024-10-09 2e4aded0-53aa-4668-b3b1-90f4747aa295,Fiona,20,Tokyo,88.64,True,inactive,2023-04-08 2f65400e-3447-4944-9db3-af09bd4d57db,John,34,Toronto,54.75,True,archived,2024-02-26 a667e148-4c64-4f5a-b23a-e00856feed8d,John,27,Singapore,33.5,True,archived,2023-02-26 517c9c76-5c3e-497e-bd78-343ebd001668,Eric,23,Sydney,78.33,True,inactive,2024-08-11 78a7f34f-7c0b-4e16-b57e-3829726569a7,Jane,32,Berlin,49.09,False,archived,2023-10-19 ================================================ FILE: data/analytics.json ================================================ [ { "id": "94efbf8b-4c95-4feb-9eda-900192276be7", "name": "Fiona", "age": 33, "city": "Singapore", "score": 95.48, "is_active": true, "status": "active", "created_at": "2024-04-30" }, { "id": "efcbb1f5-ffaf-4b40-a44b-cd67f6508eec", "name": "Alice", "age": 46, "city": "Paris", "score": 37.81, "is_active": false, "status": "active", "created_at": "2023-10-31" }, { "id": "9c2378d3-f46d-4f19-8e9e-b9053e6e57ea", "name": "Charlie", "age": 54, "city": "Tokyo", "score": 86.24, "is_active": false, "status": "archived", "created_at": "2023-11-15" }, { "id": "12a6dc88-1bd5-4a20-b729-e7a70918a60b", "name": "Charlie", "age": 31, "city": "Tokyo", "score": 61.24, "is_active": true, "status": "pending", "created_at": "2024-03-26" }, { "id": "dc1cf50e-c3c2-4843-8137-131834f7b00a", "name": "Jane", "age": 26, "city": "London", "score": 22.61, "is_active": true, "status": "inactive", "created_at": "2024-11-04" }, { "id": "db0ac268-3666-4a03-a284-e35380ae8c84", "name": "Jane", "age": 34, "city": "New York", "score": 3.1, "is_active": true, "status": "archived", "created_at": "2024-01-29" }, { "id": "24ab09dd-163a-4faf-8e06-d3fb6418dec5", "name": "Alice", "age": 64, "city": "Tokyo", "score": 6.17, "is_active": true, "status": "pending", "created_at": "2024-03-08" }, { "id": "27f8624d-c41a-4569-bbbb-1348b94fa0fd", "name": "Jane", "age": 34, "city": "Paris", "score": 43.44, "is_active": true, "status": "active", "created_at": "2023-04-19" }, { "id": "1666fe7c-a2b1-44d5-abba-293c4e0f9b23", "name": "Charlie", "age": 58, "city": "Sydney", "score": 85.02, "is_active": true, "status": "active", "created_at": "2024-09-09" }, { "id": "66036da1-7704-4ca6-9ec2-1e6d560db610", "name": "Alice", "age": 61, "city": "Singapore", "score": 93.69, "is_active": true, "status": "archived", "created_at": "2024-05-06" }, { "id": "04fa9318-9e6d-4029-be2d-9bc7ecf37f3f", "name": "John", "age": 24, "city": "Tokyo", "score": 14.59, "is_active": true, "status": "inactive", "created_at": "2023-08-17" }, { "id": "4586fbe1-acf9-4bfa-8237-ab697cdc69ed", "name": "Diana", "age": 43, "city": "Sydney", "score": 60.84, "is_active": true, "status": "archived", "created_at": "2023-11-06" }, { "id": "20b827b3-f183-44db-9f4f-f30b361c8a83", "name": "Diana", "age": 63, "city": "Singapore", "score": 96.18, "is_active": false, "status": "inactive", "created_at": "2024-08-02" }, { "id": "779dbbe0-b509-4332-98ee-d89887f48cec", "name": "Bob", "age": 47, "city": "Toronto", "score": 37.85, "is_active": false, "status": "active", "created_at": "2025-01-25" }, { "id": "8612b8d7-4524-4575-a782-a01c4a0c88a3", "name": "Bob", "age": 25, "city": "Berlin", "score": 88.28, "is_active": false, "status": "pending", "created_at": "2025-01-16" }, { "id": "ad992026-c339-40eb-a669-f16983d226ca", "name": "Diana", "age": 29, "city": "Berlin", "score": 84.17, "is_active": false, "status": "active", "created_at": "2023-07-26" }, { "id": "b3ef272f-3fc9-4422-88db-ed39858f1f68", "name": "John", "age": 20, "city": "New York", "score": 78.33, "is_active": true, "status": "archived", "created_at": "2023-04-16" }, { "id": "2a0ace4c-ee9e-4b6b-9005-897210109cca", "name": "Bob", "age": 49, "city": "Toronto", "score": 56.91, "is_active": false, "status": "active", "created_at": "2023-07-10" }, { "id": "3674700e-a83f-4450-97c8-04f80f2d2e89", "name": "Charlie", "age": 30, "city": "Sydney", "score": 80.83, "is_active": false, "status": "active", "created_at": "2024-10-22" }, { "id": "a17712d7-10f1-48e3-b1fd-8c65afb0442a", "name": "Jane", "age": 40, "city": "Toronto", "score": 35.85, "is_active": false, "status": "inactive", "created_at": "2024-02-03" }, { "id": "3b1e3e89-9ccb-46cf-9954-cb0903e8e02d", "name": "Diana", "age": 48, "city": "Singapore", "score": 37.58, "is_active": true, "status": "inactive", "created_at": "2023-06-13" }, { "id": "a4461690-a009-48c1-96d2-ed89dd1907a7", "name": "Diana", "age": 21, "city": "Tokyo", "score": 32.35, "is_active": false, "status": "pending", "created_at": "2024-06-03" }, { "id": "7157129c-0b3d-4040-8609-65afe4322ed2", "name": "Alice", "age": 65, "city": "New York", "score": 40.59, "is_active": false, "status": "inactive", "created_at": "2024-02-19" }, { "id": "dc3c472f-d16b-47e2-95c4-61c461e2b228", "name": "Diana", "age": 25, "city": "Singapore", "score": 48.32, "is_active": false, "status": "inactive", "created_at": "2024-10-03" }, { "id": "34e6e614-f376-4c1c-b3b5-f2926987115f", "name": "Bob", "age": 30, "city": "Tokyo", "score": 72.08, "is_active": false, "status": "inactive", "created_at": "2024-10-09" }, { "id": "2e4aded0-53aa-4668-b3b1-90f4747aa295", "name": "Fiona", "age": 20, "city": "Tokyo", "score": 88.64, "is_active": true, "status": "inactive", "created_at": "2023-04-08" }, { "id": "2f65400e-3447-4944-9db3-af09bd4d57db", "name": "John", "age": 34, "city": "Toronto", "score": 54.75, "is_active": true, "status": "archived", "created_at": "2024-02-26" }, { "id": "a667e148-4c64-4f5a-b23a-e00856feed8d", "name": "John", "age": 27, "city": "Singapore", "score": 33.5, "is_active": true, "status": "archived", "created_at": "2023-02-26" }, { "id": "517c9c76-5c3e-497e-bd78-343ebd001668", "name": "Eric", "age": 23, "city": "Sydney", "score": 78.33, "is_active": true, "status": "inactive", "created_at": "2024-08-11" }, { "id": "78a7f34f-7c0b-4e16-b57e-3829726569a7", "name": "Jane", "age": 32, "city": "Berlin", "score": 49.09, "is_active": false, "status": "archived", "created_at": "2023-10-19" } ] ================================================ FILE: example-agent-codebase-arch/README.md ================================================ # Example Agent Codebase Architecture This is not runnable code. It is an example of how to structure an agent codebase. ================================================ FILE: example-agent-codebase-arch/__init__.py ================================================ ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/__init__.py ================================================ ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/atom/__init__.py ================================================ """ Atomic components for the Atomic/Composable Architecture implementation of the file editor agent. These are the most basic building blocks for the file editor agent. """ ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/atom/file_tools/__init__.py ================================================ """ Atomic file operations for the Atomic/Composable Architecture implementation of the file editor agent. These are the most basic building blocks for file manipulation. """ from .result import FileOperationResult from .read import read_file from .write import write_file from .replace import replace_in_file from .insert import insert_in_file from .undo import undo_edit __all__ = [ 'FileOperationResult', 'read_file', 'write_file', 'replace_in_file', 'insert_in_file', 'undo_edit' ] ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/atom/file_tools/insert_tool.py ================================================ #!/usr/bin/env python3 """ Atomic file insert operation for the Atomic/Composable Architecture. This is the most basic building block for inserting content in files. """ import sys import os # Add the parent directory to the Python path to enable absolute imports sys.path.insert( 0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) ) from atom.path_utils.normalize import normalize_path from atom.path_utils.validation import is_valid_path, file_exists from atom.logging.console import log_info, log_error from atom.file_tools.result import FileOperationResult def insert_in_file(path: str, insert_line: int, new_str: str) -> FileOperationResult: """ Insert text at a specific line in a file. Args: path: The path to the file to modify insert_line: The line number after which to insert the text (1-indexed) new_str: The text to insert Returns: FileOperationResult with result or error message """ try: # Validate path if not is_valid_path(path): error_msg = "Invalid file path provided: path is empty." log_error("insert_in_file", error_msg) return FileOperationResult(False, error_msg) # Normalize the path path = normalize_path(path) # Check if the file exists if not file_exists(path): error_msg = f"File {path} does not exist" log_error("insert_in_file", error_msg) return FileOperationResult(False, error_msg) # Validate insert_line if insert_line is None: error_msg = "No line number specified: insert_line is missing." log_error("insert_in_file", error_msg) return FileOperationResult(False, error_msg) # Read the file with open(path, "r") as f: lines = f.readlines() # Line is 0-indexed for this function, but Claude provides 1-indexed insert_line = min(max(0, insert_line - 1), len(lines)) # Check that the index is within acceptable bounds if insert_line < 0 or insert_line > len(lines): error_msg = ( f"Insert line number {insert_line} out of range (0-{len(lines)})." ) log_error("insert_in_file", error_msg) return FileOperationResult(False, error_msg) # Ensure new_str ends with newline if new_str and not new_str.endswith("\n"): new_str += "\n" # Insert the text lines.insert(insert_line, new_str) # Write the file with open(path, "w") as f: f.writelines(lines) log_info( "insert_in_file", f"Successfully inserted text at line {insert_line + 1} in {path}", ) return FileOperationResult( True, f"Successfully inserted text at line {insert_line + 1} in {path}" ) except Exception as e: error_msg = f"Error inserting text: {str(e)}" log_error("insert_in_file", error_msg, exc_info=True) return FileOperationResult(False, error_msg) ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/atom/file_tools/read_tool.py ================================================ #!/usr/bin/env python3 """ Atomic file read operation for the Atomic/Composable Architecture. This is the most basic building block for reading files. """ import sys import os # Add the parent directory to the Python path to enable absolute imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from atom.path_utils.normalize import normalize_path from atom.path_utils.validation import is_valid_path, file_exists from atom.logging.console import log_error from atom.logging.display import display_file_content from atom.file_operations.result import FileOperationResult def read_file(path: str, start_line: int = None, end_line: int = None) -> FileOperationResult: """ Read the contents of a file. Args: path: The path to the file to read start_line: Optional start line (1-indexed) end_line: Optional end line (1-indexed, -1 for end of file) Returns: FileOperationResult with content or error message """ try: # Validate path if not is_valid_path(path): error_msg = "Invalid file path provided: path is empty." log_error("read_file", error_msg) return FileOperationResult(False, error_msg) # Normalize the path path = normalize_path(path) # Check if the file exists if not file_exists(path): error_msg = f"File {path} does not exist" log_error("read_file", error_msg) return FileOperationResult(False, error_msg) # Read the file with open(path, "r") as f: lines = f.readlines() # Apply line range if specified if start_line is not None or end_line is not None: # Convert to 0-indexed for Python start = max(0, (start_line or 1) - 1) if end_line == -1 or end_line is None: end = len(lines) else: end = min(len(lines), end_line) lines = lines[start:end] content = "".join(lines) # Display the file content (only for console, not returned to Claude) display_file_content(path, content) return FileOperationResult(True, f"Successfully read file {path}", content) except Exception as e: error_msg = f"Error reading file: {str(e)}" log_error("read_file", error_msg, exc_info=True) return FileOperationResult(False, error_msg) ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/atom/file_tools/replace_tool.py ================================================ #!/usr/bin/env python3 """ Atomic file replace operation for the Atomic/Composable Architecture. This is the most basic building block for replacing content in files. """ import sys import os # Add the parent directory to the Python path to enable absolute imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from atom.path_utils.normalize import normalize_path from atom.path_utils.validation import is_valid_path, file_exists from atom.logging.console import log_info, log_error from atom.file_operations.result import FileOperationResult def replace_in_file(path: str, old_str: str, new_str: str) -> FileOperationResult: """ Replace a string in a file. Args: path: The path to the file to modify old_str: The string to replace new_str: The string to replace with Returns: FileOperationResult with result or error message """ try: # Validate path if not is_valid_path(path): error_msg = "Invalid file path provided: path is empty." log_error("replace_in_file", error_msg) return FileOperationResult(False, error_msg) # Normalize the path path = normalize_path(path) # Check if the file exists if not file_exists(path): error_msg = f"File {path} does not exist" log_error("replace_in_file", error_msg) return FileOperationResult(False, error_msg) # Read the file with open(path, "r") as f: content = f.read() # Check if the string exists if old_str not in content: error_msg = f"The specified string was not found in the file {path}" log_error("replace_in_file", error_msg) return FileOperationResult(False, error_msg) # Replace the string new_content = content.replace(old_str, new_str, 1) # Write the file with open(path, "w") as f: f.write(new_content) log_info("replace_in_file", f"Successfully replaced text in {path}") return FileOperationResult(True, f"Successfully replaced text in {path}") except Exception as e: error_msg = f"Error replacing text: {str(e)}" log_error("replace_in_file", error_msg, exc_info=True) return FileOperationResult(False, error_msg) ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/atom/file_tools/result_tool.py ================================================ #!/usr/bin/env python3 """ Atomic file operation result model for the Atomic/Composable Architecture. This is the most basic building block for representing file operation results. """ from typing import Any, Dict class FileOperationResult: """ Model representing the result of a file operation. """ def __init__(self, success: bool, message: str, data: Any = None): """ Initialize a file operation result. Args: success: Whether the operation was successful message: A message describing the result data: Optional data returned by the operation """ self.success = success self.message = message self.data = data def to_dict(self) -> Dict[str, Any]: """ Convert the result to a dictionary. Returns: Dictionary representation of the result """ return { "success": self.success, "message": self.message, "data": self.data } def to_response(self) -> Dict[str, Any]: """ Convert the result to a response for Claude. Returns: Dictionary with result or error to send back to Claude """ if self.success: return {"result": self.data if self.data is not None else self.message} else: return {"error": self.message} ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/atom/file_tools/undo_tool.py ================================================ #!/usr/bin/env python3 """ Atomic file undo operation for the Atomic/Composable Architecture. This is the most basic building block for undoing changes to files. """ import sys import os # Add the parent directory to the Python path to enable absolute imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from atom.path_utils.normalize import normalize_path from atom.path_utils.validation import is_valid_path from atom.logging.console import log_info, log_error from atom.file_operations.result import FileOperationResult def undo_edit(path: str) -> FileOperationResult: """ Placeholder for undo_edit functionality. In a real implementation, you would need to track edit history. Args: path: The path to the file whose last edit should be undone Returns: FileOperationResult with message about undo functionality """ try: # Validate path if not is_valid_path(path): error_msg = "Invalid file path provided: path is empty." log_error("undo_edit", error_msg) return FileOperationResult(False, error_msg) # Normalize the path path = normalize_path(path) message = "Undo functionality is not implemented in this version." log_info("undo_edit", message) return FileOperationResult(True, message) except Exception as e: error_msg = f"Error in undo_edit: {str(e)}" log_error("undo_edit", error_msg, exc_info=True) return FileOperationResult(False, error_msg) ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/atom/file_tools/write_tool.py ================================================ #!/usr/bin/env python3 """ Atomic file write operation for the Atomic/Composable Architecture. This is the most basic building block for writing files. """ import sys import os # Add the parent directory to the Python path to enable absolute imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from atom.path_utils.normalize import normalize_path from atom.path_utils.validation import is_valid_path from atom.path_utils.directory import ensure_directory_exists from atom.logging.console import log_info, log_error from atom.file_operations.result import FileOperationResult def write_file(path: str, content: str) -> FileOperationResult: """ Write content to a file. Args: path: The path to the file to write content: The content to write to the file Returns: FileOperationResult with result or error message """ try: # Validate path if not is_valid_path(path): error_msg = "Invalid file path provided: path is empty." log_error("write_file", error_msg) return FileOperationResult(False, error_msg) # Normalize the path path = normalize_path(path) # Ensure the directory exists ensure_directory_exists(path) # Write the file with open(path, "w") as f: f.write(content or "") log_info("write_file", f"Successfully wrote to file {path}") return FileOperationResult(True, f"Successfully wrote to file {path}") except Exception as e: error_msg = f"Error writing file: {str(e)}" log_error("write_file", error_msg, exc_info=True) return FileOperationResult(False, error_msg) ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/atom/logging/__init__.py ================================================ """ Atomic logging utilities for the Atomic/Composable Architecture implementation of the file editor agent. These are the most basic building blocks for logging and console output. """ from .console import log_info, log_warning, log_error from .display import display_file_content, display_token_usage __all__ = [ 'log_info', 'log_warning', 'log_error', 'display_file_content', 'display_token_usage' ] ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/atom/logging/console.py ================================================ #!/usr/bin/env python3 """ Atomic console logging utilities for the Atomic/Composable Architecture. These are the most basic building blocks for console logging. """ import traceback from rich.console import Console # Initialize rich console console = Console() def log_info(component: str, message: str) -> None: """ Log an informational message. Args: component: The component that is logging the message message: The message to log """ console.log(f"[{component}] {message}") def log_warning(component: str, message: str) -> None: """ Log a warning message. Args: component: The component that is logging the message message: The message to log """ console.log(f"[{component}] [warning] {message}") console.print(f"[yellow]{message}[/yellow]") def log_error(component: str, message: str, exc_info: bool = False) -> None: """ Log an error message. Args: component: The component that is logging the message message: The message to log exc_info: Whether to include exception info """ console.log(f"[{component}] [error] {message}") console.print(f"[red]{message}[/red]") if exc_info: console.log(traceback.format_exc()) ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/atom/logging/display.py ================================================ #!/usr/bin/env python3 """ Atomic display utilities for the Atomic/Composable Architecture. These are the most basic building blocks for displaying content. """ from rich.console import Console from rich.panel import Panel from rich.syntax import Syntax from rich.table import Table import sys import os # Add the parent directory to the Python path to enable absolute imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from atom.path_utils.extension import get_file_extension # Initialize rich console console = Console() def display_file_content(path: str, content: str) -> None: """ Display file content with syntax highlighting. Args: path: Path to the file content: Content of the file """ file_extension = get_file_extension(path) syntax = Syntax(content, file_extension or "text", line_numbers=True) console.print(Panel(syntax, title=f"File: {path}")) def display_token_usage(input_tokens: int, output_tokens: int) -> None: """ Display token usage information in a rich formatted table. Args: input_tokens: Number of input tokens used output_tokens: Number of output tokens used """ total_tokens = input_tokens + output_tokens token_ratio = output_tokens / input_tokens if input_tokens > 0 else 0 # Create a table for token usage table = Table(title="Token Usage Statistics", expand=True) # Add columns with proper styling table.add_column("Metric", style="cyan", no_wrap=True) table.add_column("Count", style="magenta", justify="right") table.add_column("Percentage", justify="right") # Add rows with data table.add_row( "Input Tokens", f"{input_tokens:,}", f"{input_tokens/total_tokens:.1%}" ) table.add_row( "Output Tokens", f"{output_tokens:,}", f"{output_tokens/total_tokens:.1%}" ) table.add_row("Total Tokens", f"{total_tokens:,}", "100.0%") table.add_row("Output/Input Ratio", f"{token_ratio:.2f}", "") console.print() console.print(table) ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/atom/path_utils/__init__.py ================================================ """ Atomic path utilities for the Atomic/Composable Architecture implementation of the file editor agent. These are the most basic building blocks for path manipulation. """ from .normalize import normalize_path from .extension import get_file_extension from .directory import ensure_directory_exists from .validation import is_valid_path, file_exists __all__ = [ 'normalize_path', 'get_file_extension', 'ensure_directory_exists', 'is_valid_path', 'file_exists' ] ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/atom/path_utils/directory.py ================================================ #!/usr/bin/env python3 """ Atomic directory utility for the Atomic/Composable Architecture. This is the most basic building block for directory operations. """ import os def ensure_directory_exists(path: str) -> None: """ Ensure that the directory for a file path exists. Creates the directory if it doesn't exist. Args: path: The path to check """ directory = os.path.dirname(path) if directory and not os.path.exists(directory): os.makedirs(directory) ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/atom/path_utils/extension.py ================================================ #!/usr/bin/env python3 """ Atomic file extension utility for the Atomic/Composable Architecture. This is the most basic building block for getting file extensions. """ import os def get_file_extension(path: str) -> str: """ Get the file extension from a path. Args: path: The path to get the extension from Returns: The file extension without the dot """ return os.path.splitext(path)[1][1:] ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/atom/path_utils/normalize.py ================================================ #!/usr/bin/env python3 """ Atomic path normalization utility for the Atomic/Composable Architecture. This is the most basic building block for normalizing file paths. """ import os def normalize_path(path: str) -> str: """ Normalize file paths to handle various formats (absolute, relative, Windows paths, etc.) Args: path: The path to normalize Returns: The normalized path """ if not path: return path # Handle Windows backslash paths if provided path = path.replace("\\", os.sep) is_windows_path = False if os.name == "nt" and len(path) > 1 and path[1] == ":": is_windows_path = True # Handle /repo/ paths from Claude (tool use convention) if path.startswith("/repo/"): path = os.path.join(os.getcwd(), path[6:]) return path if path.startswith("/"): # Handle case when Claude provides paths with leading slash if path == "/" or path == "/.": # Special case for root directory path = os.getcwd() else: # Replace leading slash with current working directory path = os.path.join(os.getcwd(), path[1:]) elif path.startswith("./"): # Handle relative paths starting with ./ path = os.path.join(os.getcwd(), path[2:]) elif not os.path.isabs(path) and not is_windows_path: # For non-absolute paths that aren't Windows paths either path = os.path.join(os.getcwd(), path) return path ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/atom/path_utils/validation.py ================================================ #!/usr/bin/env python3 """ Atomic path validation utilities for the Atomic/Composable Architecture. These are the most basic building blocks for validating paths. """ import os def is_valid_path(path: str) -> bool: """ Check if a path is valid. Args: path: The path to check Returns: True if the path is valid, False otherwise """ return path is not None and path.strip() != "" def file_exists(path: str) -> bool: """ Check if a file exists. Args: path: The path to check Returns: True if the file exists, False otherwise """ return os.path.exists(path) and os.path.isfile(path) ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/membrane/__init__.py ================================================ ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/membrane/main_file_agent.py ================================================ #!/usr/bin/env python3 """ Organism-level file agent for the Atomic/Composable Architecture implementation of the file editor agent. This module pulls together all components to provide a high-level API for the file editor agent. """ import sys import os import json import argparse import traceback from typing import Dict, Any, Optional, List, Union from rich.console import Console # Add the parent directory to the Python path to enable absolute imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import anthropic from molecule.file_crud import FileCRUD from organism.file_agent import run_agent from atom.logging.console import log_info, log_error, log_warning from atom.logging.display import display_token_usage class FileAgent: """ File agent that pulls together all components to provide a high-level API for the file editor agent. """ @staticmethod def run(prompt: str, api_key: Optional[str] = None, max_tool_use_loops: int = 15, token_efficient_tool_use: bool = True) -> None: """ Run the file editor agent with the specified prompt. Args: prompt: The prompt to send to Claude api_key: Optional API key for Anthropic max_tool_use_loops: Maximum number of tool use loops token_efficient_tool_use: Whether to use token-efficient tool use """ log_info("file_agent", f"Running file editor agent with prompt: {prompt}") # Get the API key api_key = api_key or os.environ.get("ANTHROPIC_API_KEY") if not api_key: log_error("file_agent", "No API key provided. Please set the ANTHROPIC_API_KEY environment variable or provide an API key.") # For testing purposes, we'll just print a success message console = Console() console.print("[green]Successfully loaded the Atomic/Composable Architecture implementation![/green]") console.print("[yellow]This is a mock implementation for testing the architecture structure.[/yellow]") console.print("[yellow]In a real implementation, this would connect to the Claude API.[/yellow]") # Display mock token usage display_token_usage(1000, 500) return # Initialize the Anthropic client client = anthropic.Anthropic(api_key=api_key) # Run the agent try: input_tokens, output_tokens = run_agent( client=client, prompt=prompt, handle_tool_use=FileCRUD.handle_tool_use, max_tool_use_loops=max_tool_use_loops, token_efficient_tool_use=token_efficient_tool_use ) # Display token usage display_token_usage(input_tokens, output_tokens) log_info("file_agent", "File editor agent completed successfully.") except Exception as e: log_error("file_agent", f"Error running file editor agent: {str(e)}", exc_info=True) def main(): """ Main entry point for the file editor agent. """ parser = argparse.ArgumentParser(description="File Editor Agent") parser.add_argument("--prompt", type=str, help="Prompt to send to Claude") parser.add_argument("--api-key", type=str, help="API key for Anthropic") parser.add_argument("--max-tool-use-loops", type=int, default=15, help="Maximum number of tool use loops") parser.add_argument("--token-efficient-tool-use", action="store_true", help="Use token-efficient tool use") args = parser.parse_args() if not args.prompt: log_error("main", "No prompt provided. Please provide a prompt with --prompt.") return FileAgent.run( prompt=args.prompt, api_key=args.api_key, max_tool_use_loops=args.max_tool_use_loops, token_efficient_tool_use=args.token_efficient_tool_use ) if __name__ == "__main__": main() ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/membrane/mcp_file_agent.py ================================================ #!/usr/bin/env python3 """ Organism-level file agent for the Atomic/Composable Architecture implementation of the file editor agent. This module pulls together all components to provide a high-level API for the file editor agent. """ import sys import os import json import argparse import traceback from typing import Dict, Any, Optional, List, Union from rich.console import Console # Add the parent directory to the Python path to enable absolute imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import anthropic from molecule.file_crud import FileCRUD from organism.file_agent import run_agent from atom.logging.console import log_info, log_error, log_warning from atom.logging.display import display_token_usage class FileAgent: """ File agent that pulls together all components to provide a high-level API for the file editor agent. """ @staticmethod def run(prompt: str, api_key: Optional[str] = None, max_tool_use_loops: int = 15, token_efficient_tool_use: bool = True) -> None: """ Run the file editor agent with the specified prompt. Args: prompt: The prompt to send to Claude api_key: Optional API key for Anthropic max_tool_use_loops: Maximum number of tool use loops token_efficient_tool_use: Whether to use token-efficient tool use """ log_info("file_agent", f"Running file editor agent with prompt: {prompt}") # Get the API key api_key = api_key or os.environ.get("ANTHROPIC_API_KEY") if not api_key: log_error("file_agent", "No API key provided. Please set the ANTHROPIC_API_KEY environment variable or provide an API key.") # For testing purposes, we'll just print a success message console = Console() console.print("[green]Successfully loaded the Atomic/Composable Architecture implementation![/green]") console.print("[yellow]This is a mock implementation for testing the architecture structure.[/yellow]") console.print("[yellow]In a real implementation, this would connect to the Claude API.[/yellow]") # Display mock token usage display_token_usage(1000, 500) return # Initialize the Anthropic client client = anthropic.Anthropic(api_key=api_key) # Run the agent try: input_tokens, output_tokens = run_agent( client=client, prompt=prompt, handle_tool_use=FileCRUD.handle_tool_use, max_tool_use_loops=max_tool_use_loops, token_efficient_tool_use=token_efficient_tool_use ) # Display token usage display_token_usage(input_tokens, output_tokens) log_info("file_agent", "File editor agent completed successfully.") except Exception as e: log_error("file_agent", f"Error running file editor agent: {str(e)}", exc_info=True) def main(): """ Main entry point for the file editor agent. """ parser = argparse.ArgumentParser(description="File Editor Agent") parser.add_argument("--prompt", type=str, help="Prompt to send to Claude") parser.add_argument("--api-key", type=str, help="API key for Anthropic") parser.add_argument("--max-tool-use-loops", type=int, default=15, help="Maximum number of tool use loops") parser.add_argument("--token-efficient-tool-use", action="store_true", help="Use token-efficient tool use") args = parser.parse_args() if not args.prompt: log_error("main", "No prompt provided. Please provide a prompt with --prompt.") return FileAgent.run( prompt=args.prompt, api_key=args.api_key, max_tool_use_loops=args.max_tool_use_loops, token_efficient_tool_use=args.token_efficient_tool_use ) if __name__ == "__main__": main() ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/molecule/__init__.py ================================================ """ Molecular components for the Atomic/Composable Architecture implementation of the file editor agent. These components combine atomic building blocks to provide higher-level functionality. """ ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/molecule/file_crud.py ================================================ #!/usr/bin/env python3 """ Molecular file CRUD operations for the Atomic/Composable Architecture implementation of the file editor agent. This module combines atomic components to provide file CRUD capabilities. """ import sys import os # Add the parent directory to the Python path to enable absolute imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from atom.file_operations import read_file, write_file, replace_in_file, insert_in_file, undo_edit, FileOperationResult from atom.logging.console import log_info, log_error class FileCRUD: """ File CRUD operations that combine atomic components to provide file manipulation capabilities. """ @staticmethod def read(path: str, start_line: int = None, end_line: int = None) -> FileOperationResult: """ Read the contents of a file. Args: path: The path to the file to read start_line: Optional start line (1-indexed) end_line: Optional end line (1-indexed, -1 for end of file) Returns: FileOperationResult with content or error message """ log_info("file_crud", f"Reading file {path} with range {start_line}-{end_line}") result = read_file(path, start_line, end_line) if result.success: log_info("file_crud", f"Successfully read file {path}") else: log_error("file_crud", f"Failed to read file {path}: {result.message}") return result @staticmethod def write(path: str, content: str) -> FileOperationResult: """ Write content to a file. Args: path: The path to the file to write content: The content to write to the file Returns: FileOperationResult with result or error message """ log_info("file_crud", f"Writing to file {path}") result = write_file(path, content) if result.success: log_info("file_crud", f"Successfully wrote to file {path}") else: log_error("file_crud", f"Failed to write to file {path}: {result.message}") return result @staticmethod def replace(path: str, old_str: str, new_str: str) -> FileOperationResult: """ Replace a string in a file. Args: path: The path to the file to modify old_str: The string to replace new_str: The string to replace with Returns: FileOperationResult with result or error message """ log_info("file_crud", f"Replacing text in file {path}") result = replace_in_file(path, old_str, new_str) if result.success: log_info("file_crud", f"Successfully replaced text in file {path}") else: log_error("file_crud", f"Failed to replace text in file {path}: {result.message}") return result @staticmethod def insert(path: str, insert_line: int, new_str: str) -> FileOperationResult: """ Insert text at a specific line in a file. Args: path: The path to the file to modify insert_line: The line number after which to insert the text (1-indexed) new_str: The text to insert Returns: FileOperationResult with result or error message """ log_info("file_crud", f"Inserting text at line {insert_line} in file {path}") result = insert_in_file(path, insert_line, new_str) if result.success: log_info("file_crud", f"Successfully inserted text at line {insert_line} in file {path}") else: log_error("file_crud", f"Failed to insert text at line {insert_line} in file {path}: {result.message}") return result @staticmethod def create(path: str, content: str) -> FileOperationResult: """ Create a new file with the specified content. Args: path: The path to the file to create content: The content to write to the file Returns: FileOperationResult with result or error message """ log_info("file_crud", f"Creating file {path}") return FileCRUD.write(path, content) @staticmethod def undo(path: str) -> FileOperationResult: """ Undo the last edit to a file. Args: path: The path to the file whose last edit should be undone Returns: FileOperationResult with message about undo functionality """ log_info("file_crud", f"Undoing last edit to file {path}") result = undo_edit(path) if result.success: log_info("file_crud", f"Successfully undid last edit to file {path}") else: log_error("file_crud", f"Failed to undo last edit to file {path}: {result.message}") return result @staticmethod def handle_tool_use(tool_use: dict) -> dict: """ Handle a tool use request from Claude. Args: tool_use: The tool use request from Claude Returns: Dictionary with result or error to send back to Claude """ command = tool_use.get("command") path = tool_use.get("path") log_info("file_crud", f"Handling tool use request: {command} on {path}") if not command: error_msg = "No command specified in tool use request" log_error("file_crud", error_msg) return {"error": error_msg} if not path and command != "undo_edit": error_msg = "No path specified in tool use request" log_error("file_crud", error_msg) return {"error": error_msg} result = None try: if command == "view": view_range = tool_use.get("view_range") start_line = None end_line = None if view_range: start_line, end_line = view_range result = FileCRUD.read(path, start_line, end_line) elif command == "str_replace": old_str = tool_use.get("old_str") new_str = tool_use.get("new_str") if old_str is None: return {"error": "Missing 'old_str' parameter for str_replace command"} if new_str is None: return {"error": "Missing 'new_str' parameter for str_replace command"} result = FileCRUD.replace(path, old_str, new_str) elif command == "create": file_text = tool_use.get("file_text", "") result = FileCRUD.create(path, file_text) elif command == "insert": insert_line = tool_use.get("insert_line") new_str = tool_use.get("new_str") if insert_line is None: return {"error": "Missing 'insert_line' parameter for insert command"} if new_str is None: return {"error": "Missing 'new_str' parameter for insert command"} result = FileCRUD.insert(path, insert_line, new_str) elif command == "undo_edit": result = FileCRUD.undo(path) else: error_msg = f"Unknown command: {command}" log_error("file_crud", error_msg) return {"error": error_msg} # Convert the result to a response for Claude if result.success: return {"result": result.data if result.data is not None else result.message} else: return {"error": result.message} except Exception as e: error_msg = f"Error handling tool use: {str(e)}" log_error("file_crud", error_msg, exc_info=True) return {"error": error_msg} ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/molecule/file_reader.py ================================================ #!/usr/bin/env python3 """ Molecular file reader for the Atomic/Composable Architecture implementation of the file editor agent. This module combines atomic components to provide file reading capabilities. """ import sys import os # Add the parent directory to the Python path to enable absolute imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from atom.file_operations import read_file, FileOperationResult from atom.logging.console import log_info, log_error class FileReader: """ File reader that combines atomic components to provide file reading capabilities. """ @staticmethod def read(path: str, start_line: int = None, end_line: int = None) -> FileOperationResult: """ Read the contents of a file. Args: path: The path to the file to read start_line: Optional start line (1-indexed) end_line: Optional end line (1-indexed, -1 for end of file) Returns: FileOperationResult with content or error message """ log_info("file_reader", f"Reading file {path} with range {start_line}-{end_line}") # Use the atomic read_file function result = read_file(path, start_line, end_line) if result.success: log_info("file_reader", f"Successfully read file {path}") else: log_error("file_reader", f"Failed to read file {path}: {result.message}") return result @staticmethod def view_file(path: str, view_range=None) -> FileOperationResult: """ View the contents of a file with optional range. Args: path: The path to the file to view view_range: Optional tuple of (start_line, end_line) Returns: FileOperationResult with content or error message """ start_line = None end_line = None if view_range: start_line, end_line = view_range log_info("file_reader", f"Viewing file {path} with range {start_line}-{end_line}") return FileReader.read(path, start_line, end_line) ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/molecule/file_writer.py ================================================ #!/usr/bin/env python3 """ Molecular file writer for the Atomic/Composable Architecture implementation of the file editor agent. This module combines atomic components to provide file writing capabilities. """ import sys import os # Add the parent directory to the Python path to enable absolute imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from atom.file_operations import write_file, replace_in_file, insert_in_file, FileOperationResult from atom.logging.console import log_info, log_error class FileWriter: """ File writer that combines atomic components to provide file writing capabilities. """ @staticmethod def write(path: str, content: str) -> FileOperationResult: """ Write content to a file. Args: path: The path to the file to write content: The content to write to the file Returns: FileOperationResult with result or error message """ log_info("file_writer", f"Writing to file {path}") # Use the atomic write_file function result = write_file(path, content) if result.success: log_info("file_writer", f"Successfully wrote to file {path}") else: log_error("file_writer", f"Failed to write to file {path}: {result.message}") return result @staticmethod def replace(path: str, old_str: str, new_str: str) -> FileOperationResult: """ Replace a string in a file. Args: path: The path to the file to modify old_str: The string to replace new_str: The string to replace with Returns: FileOperationResult with result or error message """ log_info("file_writer", f"Replacing text in file {path}") # Use the atomic replace_in_file function result = replace_in_file(path, old_str, new_str) if result.success: log_info("file_writer", f"Successfully replaced text in file {path}") else: log_error("file_writer", f"Failed to replace text in file {path}: {result.message}") return result @staticmethod def insert(path: str, insert_line: int, new_str: str) -> FileOperationResult: """ Insert text at a specific line in a file. Args: path: The path to the file to modify insert_line: The line number after which to insert the text (1-indexed) new_str: The text to insert Returns: FileOperationResult with result or error message """ log_info("file_writer", f"Inserting text at line {insert_line} in file {path}") # Use the atomic insert_in_file function result = insert_in_file(path, insert_line, new_str) if result.success: log_info("file_writer", f"Successfully inserted text at line {insert_line} in file {path}") else: log_error("file_writer", f"Failed to insert text at line {insert_line} in file {path}: {result.message}") return result @staticmethod def create(path: str, content: str) -> FileOperationResult: """ Create a new file with the specified content. Args: path: The path to the file to create content: The content to write to the file Returns: FileOperationResult with result or error message """ log_info("file_writer", f"Creating file {path}") # Use the atomic write_file function return FileWriter.write(path, content) ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/organism/__init__.py ================================================ """ Organism-level components for the Atomic/Composable Architecture implementation of the file editor agent. These components pull together molecular components to provide high-level APIs. """ ================================================ FILE: example-agent-codebase-arch/atomic-composable-architecture/organism/file_agent.py ================================================ #!/usr/bin/env python3 """ Molecular file editor for the Atomic/Composable Architecture implementation of the file editor agent. This module combines atomic components to provide file editing capabilities. """ import time from typing import Tuple, Dict, Any, List, Optional, Callable from rich.console import Console from rich.panel import Panel from rich.markdown import Markdown from anthropic import Anthropic import sys import os # Add the parent directory to the Python path to enable absolute imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from atom.logging import log_info, log_error, display_token_usage # Initialize rich console console = Console() # Define constants MODEL = "claude-3-7-sonnet-20250219" DEFAULT_THINKING_TOKENS = 3000 class FileEditor: """ File editor that combines atomic components to provide file editing capabilities. """ @staticmethod def run_agent( client: Anthropic, prompt: str, handle_tool_use_func, max_thinking_tokens: int = DEFAULT_THINKING_TOKENS, max_loops: int = 10, use_token_efficiency: bool = False, ) -> Tuple[str, int, int]: """ Run the Claude agent with file editing capabilities. Args: client: The Anthropic client prompt: The user's prompt handle_tool_use_func: Function to handle tool use requests max_thinking_tokens: Maximum tokens for thinking max_loops: Maximum number of tool use loops use_token_efficiency: Whether to use token-efficient tool use beta feature Returns: Tuple containing: - Final response from Claude (str) - Total input tokens used (int) - Total output tokens used (int) """ # Track token usage input_tokens_total = 0 output_tokens_total = 0 system_prompt = """You are a helpful AI assistant with text editing capabilities. You have access to a text editor tool that can view, edit, and create files. Always think step by step about what you need to do before taking any action. Be careful when making edits to files, as they can permanently change the user's files. Follow these steps when handling file operations: 1. First, view files to understand their content before making changes 2. For edits, ensure you have the correct context and are making the right changes 3. When creating files, make sure they're in the right location with proper formatting """ # Define text editor tool text_editor_tool = {"name": "str_replace_editor", "type": "text_editor_20250124"} messages = [ { "role": "user", "content": f"""I need help with editing files. Here's what I want to do: {prompt} Please use the text editor tool to help me with this. First, think through what you need to do, then use the appropriate tool. """, } ] loop_count = 0 tool_use_count = 0 thinking_start_time = time.time() while loop_count < max_loops: loop_count += 1 console.rule(f"[yellow]Agent Loop {loop_count}/{max_loops}[/yellow]") log_info("file_editor", f"Starting agent loop {loop_count}/{max_loops}") # Create message with text editor tool message_args = { "model": MODEL, "max_tokens": 4096, "tools": [text_editor_tool], "messages": messages, "system": system_prompt, "thinking": {"type": "enabled", "budget_tokens": max_thinking_tokens}, } # Use the beta.messages with betas parameter if token efficiency is enabled if use_token_efficiency: # Using token-efficient tools beta feature message_args["betas"] = ["token-efficient-tools-2025-02-19"] response = client.beta.messages.create(**message_args) else: # Standard approach response = client.messages.create(**message_args) # Track token usage if hasattr(response, "usage"): input_tokens = getattr(response.usage, "input_tokens", 0) output_tokens = getattr(response.usage, "output_tokens", 0) input_tokens_total += input_tokens output_tokens_total += output_tokens console.print( f"[dim]Loop {loop_count} tokens: Input={input_tokens}, Output={output_tokens}[/dim]" ) log_info( "file_editor", f"Loop {loop_count} tokens: Input={input_tokens}, Output={output_tokens}" ) # Process response content thinking_block = None tool_use_block = None text_block = None for content_block in response.content: if content_block.type == "thinking": thinking_block = content_block # Access the thinking attribute which contains the actual thinking text if hasattr(thinking_block, "thinking"): console.print( Panel( thinking_block.thinking, title=f"Claude's Thinking (Loop {loop_count})", border_style="blue", ) ) else: console.print( Panel( "Claude is thinking...", title=f"Claude's Thinking (Loop {loop_count})", border_style="blue", ) ) elif content_block.type == "tool_use": tool_use_block = content_block tool_use_count += 1 elif content_block.type == "text": text_block = content_block # If we got a final text response with no tool use, we're done if text_block and not tool_use_block: thinking_end_time = time.time() thinking_duration = thinking_end_time - thinking_start_time console.print( f"\n[bold green]Completed in {thinking_duration:.2f} seconds after {loop_count} loops and {tool_use_count} tool uses[/bold green]" ) log_info( "file_editor", f"Completed in {thinking_duration:.2f} seconds after {loop_count} loops and {tool_use_count} tool uses" ) # Add the response to messages messages.append( { "role": "assistant", "content": [ *([thinking_block] if thinking_block else []), {"type": "text", "text": text_block.text}, ], } ) return text_block.text, input_tokens_total, output_tokens_total # Handle tool use if tool_use_block: # Add the assistant's response to messages before handling tool calls messages.append({"role": "assistant", "content": response.content}) console.print( f"\n[bold blue]Tool Call:[/bold blue] {tool_use_block.name}" ) log_info("file_editor", f"Tool Call: {tool_use_block.name}") # Handle the tool use tool_result = handle_tool_use_func(tool_use_block.input) # Format tool result for Claude tool_result_message = { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_use_block.id, "content": tool_result.get("error") or tool_result.get("result", ""), } ], } messages.append(tool_result_message) # If we reach here, we hit the max loops console.print( f"\n[bold red]Warning: Reached maximum loops ({max_loops}) without completing the task[/bold red]" ) log_error( "file_editor", f"Reached maximum loops ({max_loops}) without completing the task" ) return ( "I wasn't able to complete the task within the allowed number of thinking steps. Please try a more specific prompt or increase the loop limit.", input_tokens_total, output_tokens_total, ) # Expose the run_agent function at the module level def run_agent( client: Anthropic, prompt: str, handle_tool_use: Callable[[Dict[str, Any]], Dict[str, Any]], max_tool_use_loops: int = 15, token_efficient_tool_use: bool = True, ) -> Tuple[int, int]: """ Run the file editor agent with the specified prompt. Args: client: The Anthropic client prompt: The prompt to send to Claude handle_tool_use: Function to handle tool use requests max_tool_use_loops: Maximum number of tool use loops token_efficient_tool_use: Whether to use token-efficient tool use Returns: Tuple containing input and output token counts """ log_info("file_editor", f"Running agent with prompt: {prompt}") _, input_tokens, output_tokens = FileEditor.run_agent( client=client, prompt=prompt, handle_tool_use_func=handle_tool_use, max_loops=max_tool_use_loops, use_token_efficiency=token_efficient_tool_use, max_thinking_tokens=DEFAULT_THINKING_TOKENS ) return input_tokens, output_tokens ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/__init__.py ================================================ ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/__init__.py ================================================ ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/blog_agent/__init__.py ================================================ """ Blog agent package for the Vertical Slice Architecture. This package provides blog management capabilities. """ from features.blog_agent.blog_agent import run_agent from features.blog_agent.blog_manager import BlogManager ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/blog_agent/blog_agent.py ================================================ #!/usr/bin/env python3 """ Blog agent for the Vertical Slice Architecture implementation of the blog agent. This module provides the agent interface for blog management operations. """ import time from typing import Tuple, Dict, Any, List, Optional from rich.console import Console from rich.panel import Panel from rich.markdown import Markdown from anthropic import Anthropic import sys import os # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error, display_token_usage from features.blog_agent.tool_handler import handle_tool_use # Initialize rich console console = Console() # Define constants MODEL = "claude-3-7-sonnet-20250219" DEFAULT_THINKING_TOKENS = 3000 class BlogAgent: """ Blog agent that provides an interface for AI-assisted blog management. """ @staticmethod def run_agent( client: Anthropic, prompt: str, max_thinking_tokens: int = DEFAULT_THINKING_TOKENS, max_loops: int = 10, use_token_efficiency: bool = False, ) -> Tuple[str, int, int]: """ Run the Claude agent with blog management capabilities. Args: client: The Anthropic client prompt: The user's prompt max_thinking_tokens: Maximum tokens for thinking max_loops: Maximum number of tool use loops use_token_efficiency: Whether to use token-efficient tool use beta feature Returns: Tuple containing: - Final response from Claude (str) - Total input tokens used (int) - Total output tokens used (int) """ # Track token usage input_tokens_total = 0 output_tokens_total = 0 system_prompt = """You are a helpful AI assistant with blog management capabilities. You have access to tools that can create, read, update, delete, and search blog posts. Always think step by step about what you need to do before taking any action. Be helpful in suggesting blog post ideas and improvements when asked. Available commands: - create_post: Create a new blog post (title, content, author, tags) - get_post: Get a blog post by ID (post_id) - update_post: Update a blog post (post_id, title?, content?, tags?, published?) - delete_post: Delete a blog post (post_id) - list_posts: List blog posts (tag?, author?, published_only?) - search_posts: Search blog posts (query, search_content?, tag?, author?) - publish_post: Publish a blog post (post_id) - unpublish_post: Unpublish a blog post (post_id) """ # Define blog management tool blog_management_tool = { "name": "blog_management", "description": "Manage blog posts including creation, editing, searching, and publishing", "input_schema": { "type": "object", "properties": { "command": { "type": "string", "enum": [ "create_post", "get_post", "update_post", "delete_post", "list_posts", "search_posts", "publish_post", "unpublish_post" ], "description": "The blog management command to execute" } }, "required": ["command"] } } messages = [ { "role": "user", "content": f"""I need help managing my blog. Here's what I want to do: {prompt} Please use the blog management tools to help me with this. First, think through what you need to do, then use the appropriate tools. """, } ] loop_count = 0 tool_use_count = 0 thinking_start_time = time.time() while loop_count < max_loops: loop_count += 1 console.rule(f"[yellow]Agent Loop {loop_count}/{max_loops}[/yellow]") log_info("blog_agent", f"Starting agent loop {loop_count}/{max_loops}") # Create message with blog management tool message_args = { "model": MODEL, "max_tokens": 4096, "tools": [blog_management_tool], "messages": messages, "system": system_prompt, "thinking": {"type": "enabled", "budget_tokens": max_thinking_tokens}, } # Use the beta.messages with betas parameter if token efficiency is enabled if use_token_efficiency: # Using token-efficient tools beta feature message_args["betas"] = ["token-efficient-tools-2025-02-19"] response = client.beta.messages.create(**message_args) else: # Standard approach response = client.messages.create(**message_args) # Track token usage if hasattr(response, "usage"): input_tokens = getattr(response.usage, "input_tokens", 0) output_tokens = getattr(response.usage, "output_tokens", 0) input_tokens_total += input_tokens output_tokens_total += output_tokens console.print( f"[dim]Loop {loop_count} tokens: Input={input_tokens}, Output={output_tokens}[/dim]" ) log_info( "blog_agent", f"Loop {loop_count} tokens: Input={input_tokens}, Output={output_tokens}" ) # Process response content thinking_block = None tool_use_block = None text_block = None for content_block in response.content: if content_block.type == "thinking": thinking_block = content_block # Access the thinking attribute which contains the actual thinking text if hasattr(thinking_block, "thinking"): console.print( Panel( thinking_block.thinking, title=f"Claude's Thinking (Loop {loop_count})", border_style="blue", ) ) else: console.print( Panel( "Claude is thinking...", title=f"Claude's Thinking (Loop {loop_count})", border_style="blue", ) ) elif content_block.type == "tool_use": tool_use_block = content_block tool_use_count += 1 elif content_block.type == "text": text_block = content_block # If we got a final text response with no tool use, we're done if text_block and not tool_use_block: thinking_end_time = time.time() thinking_duration = thinking_end_time - thinking_start_time console.print( f"\n[bold green]Completed in {thinking_duration:.2f} seconds after {loop_count} loops and {tool_use_count} tool uses[/bold green]" ) log_info( "blog_agent", f"Completed in {thinking_duration:.2f} seconds after {loop_count} loops and {tool_use_count} tool uses" ) # Add the response to messages messages.append( { "role": "assistant", "content": [ *([thinking_block] if thinking_block else []), {"type": "text", "text": text_block.text}, ], } ) return text_block.text, input_tokens_total, output_tokens_total # Handle tool use if tool_use_block: # Add the assistant's response to messages before handling tool calls messages.append({"role": "assistant", "content": response.content}) console.print( f"\n[bold blue]Tool Call:[/bold blue] {tool_use_block.name}" ) log_info("blog_agent", f"Tool Call: {tool_use_block.name}") # Handle the tool use tool_result = handle_tool_use(tool_use_block.input) # Format tool result for Claude tool_result_message = { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_use_block.id, "content": tool_result.get("error") or tool_result.get("result", ""), } ], } # If we have data in the result, include it as formatted markdown if "data" in tool_result and tool_result["data"]: data_json = json.dumps(tool_result["data"], indent=2) tool_result_message["content"][0]["content"] += f"\n\n```json\n{data_json}\n```" messages.append(tool_result_message) # If we reach here, we hit the max loops console.print( f"\n[bold red]Warning: Reached maximum loops ({max_loops}) without completing the task[/bold red]" ) log_error( "blog_agent", f"Reached maximum loops ({max_loops}) without completing the task" ) return ( "I wasn't able to complete the task within the allowed number of thinking steps. Please try a more specific prompt or increase the loop limit.", input_tokens_total, output_tokens_total, ) # Expose the run_agent function at the module level def run_agent( client: Anthropic, prompt: str, max_tool_use_loops: int = 15, token_efficient_tool_use: bool = True, ) -> Tuple[int, int]: """ Run the blog agent with the specified prompt. Args: client: The Anthropic client prompt: The prompt to send to Claude max_tool_use_loops: Maximum number of tool use loops token_efficient_tool_use: Whether to use token-efficient tool use Returns: Tuple containing input and output token counts """ log_info("blog_agent", f"Running agent with prompt: {prompt}") _, input_tokens, output_tokens = BlogAgent.run_agent( client=client, prompt=prompt, max_loops=max_tool_use_loops, use_token_efficiency=token_efficient_tool_use, max_thinking_tokens=DEFAULT_THINKING_TOKENS ) return input_tokens, output_tokens ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/blog_agent/blog_manager.py ================================================ #!/usr/bin/env python3 """ Blog manager for the Vertical Slice Architecture implementation of the blog agent. This module combines various blog tools to provide comprehensive blog management capabilities. """ import sys import os from typing import Dict, Any, List, Optional # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.blog_agent.model_tools import BlogOperationResult from features.blog_agent.create_tool import create_blog_post from features.blog_agent.read_tool import read_blog_post, list_blog_posts from features.blog_agent.update_tool import update_blog_post, publish_blog_post, unpublish_blog_post from features.blog_agent.delete_tool import delete_blog_post from features.blog_agent.search_tool import search_blog_posts class BlogManager: """ Blog manager that combines various tools to provide blog management capabilities. """ @staticmethod def create_post(title: str, content: str, author: str, tags: List[str] = None) -> BlogOperationResult: """ Create a new blog post. Args: title: The title of the blog post content: The content of the blog post author: The author of the blog post tags: Optional list of tags Returns: BlogOperationResult with result or error message """ log_info("blog_manager", f"Creating blog post: {title}") return create_blog_post(title, content, author, tags) @staticmethod def get_post(post_id: str) -> BlogOperationResult: """ Get a blog post by ID. Args: post_id: The ID of the blog post to get Returns: BlogOperationResult with the blog post or error message """ log_info("blog_manager", f"Getting blog post: {post_id}") return read_blog_post(post_id) @staticmethod def update_post(post_id: str, title: Optional[str] = None, content: Optional[str] = None, tags: Optional[List[str]] = None, published: Optional[bool] = None) -> BlogOperationResult: """ Update a blog post. Args: post_id: The ID of the blog post to update title: Optional new title content: Optional new content tags: Optional new tags published: Optional new publication status Returns: BlogOperationResult with the updated blog post or error message """ log_info("blog_manager", f"Updating blog post: {post_id}") return update_blog_post(post_id, title, content, tags, published) @staticmethod def delete_post(post_id: str) -> BlogOperationResult: """ Delete a blog post. Args: post_id: The ID of the blog post to delete Returns: BlogOperationResult with result or error message """ log_info("blog_manager", f"Deleting blog post: {post_id}") return delete_blog_post(post_id) @staticmethod def list_posts(tag: Optional[str] = None, author: Optional[str] = None, published_only: bool = False) -> BlogOperationResult: """ List blog posts, optionally filtered by tag, author, or publication status. Args: tag: Optional tag to filter by author: Optional author to filter by published_only: Whether to only return published posts Returns: BlogOperationResult with a list of blog posts or error message """ log_info("blog_manager", "Listing blog posts") return list_blog_posts(tag, author, published_only) @staticmethod def search_posts(query: str, search_content: bool = True, tag: Optional[str] = None, author: Optional[str] = None) -> BlogOperationResult: """ Search blog posts by query string, optionally filtered by tag or author. Args: query: The search query search_content: Whether to search in the content (otherwise just title and tags) tag: Optional tag to filter by author: Optional author to filter by Returns: BlogOperationResult with a list of matching blog posts or error message """ log_info("blog_manager", f"Searching blog posts for: {query}") return search_blog_posts(query, search_content, tag, author) @staticmethod def publish_post(post_id: str) -> BlogOperationResult: """ Publish a blog post. Args: post_id: The ID of the blog post to publish Returns: BlogOperationResult with the published blog post or error message """ log_info("blog_manager", f"Publishing blog post: {post_id}") return publish_blog_post(post_id) @staticmethod def unpublish_post(post_id: str) -> BlogOperationResult: """ Unpublish a blog post. Args: post_id: The ID of the blog post to unpublish Returns: BlogOperationResult with the unpublished blog post or error message """ log_info("blog_manager", f"Unpublishing blog post: {post_id}") return unpublish_blog_post(post_id) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/blog_agent/create_tool.py ================================================ #!/usr/bin/env python3 """ Create tool for the blog agent in the Vertical Slice Architecture. This module provides blog post creation capabilities. """ import sys import os import json import uuid from datetime import datetime from typing import Dict, Any # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.blog_agent.model_tools import BlogPost, BlogOperationResult # Path to store blog posts BLOG_POSTS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "data", "blog_posts") def create_blog_post(title: str, content: str, author: str, tags: list = None) -> BlogOperationResult: """ Create a new blog post. Args: title: Title of the blog post content: Content of the blog post author: Author of the blog post tags: Optional list of tags Returns: BlogOperationResult with result or error message """ log_info("create_tool", f"Creating blog post: {title}") try: # Create directory if it doesn't exist os.makedirs(BLOG_POSTS_DIR, exist_ok=True) # Generate a unique ID and timestamps post_id = str(uuid.uuid4()) current_time = datetime.now().isoformat() # Create the blog post blog_post = BlogPost( id=post_id, title=title, content=content, author=author, tags=tags or [], published=False, created_at=current_time, updated_at=current_time ) # Save the blog post to a JSON file file_path = os.path.join(BLOG_POSTS_DIR, f"{post_id}.json") with open(file_path, 'w', encoding='utf-8') as f: json.dump(blog_post.to_dict(), f, indent=2) log_info("create_tool", f"Created blog post: {title} with ID: {post_id}") return BlogOperationResult( success=True, message=f"Successfully created blog post: {title}", data=blog_post.to_dict() ) except Exception as e: error_msg = f"Failed to create blog post: {str(e)}" log_error("create_tool", error_msg) return BlogOperationResult(success=False, message=error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/blog_agent/delete_tool.py ================================================ #!/usr/bin/env python3 """ Delete tool for the blog agent in the Vertical Slice Architecture. This module provides blog post deletion capabilities. """ import sys import os import json from typing import Dict, Any # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.blog_agent.model_tools import BlogOperationResult from features.blog_agent.read_tool import read_blog_post # Path to store blog posts BLOG_POSTS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "data", "blog_posts") def delete_blog_post(post_id: str) -> BlogOperationResult: """ Delete a blog post by ID. Args: post_id: The ID of the blog post to delete Returns: BlogOperationResult with result or error message """ log_info("delete_tool", f"Deleting blog post with ID: {post_id}") try: # Verify the blog post exists read_result = read_blog_post(post_id) if not read_result.success: return read_result # Get the blog post title for the response message blog_post_title = read_result.data["title"] # Delete the blog post file file_path = os.path.join(BLOG_POSTS_DIR, f"{post_id}.json") os.remove(file_path) log_info("delete_tool", f"Deleted blog post: {blog_post_title}") return BlogOperationResult( success=True, message=f"Successfully deleted blog post: {blog_post_title}" ) except Exception as e: error_msg = f"Failed to delete blog post: {str(e)}" log_error("delete_tool", error_msg) return BlogOperationResult(success=False, message=error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/blog_agent/model_tools.py ================================================ #!/usr/bin/env python3 """ Models for the blog agent in the Vertical Slice Architecture. """ import os import sys from typing import Dict, Any, Optional, List, Union from dataclasses import dataclass # Add the project root to the Python path sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../'))) @dataclass class BlogPost: """Model representing a blog post.""" title: str content: str author: str tags: List[str] published: bool = False id: Optional[str] = None created_at: Optional[str] = None updated_at: Optional[str] = None def to_dict(self) -> Dict[str, Any]: """Convert the blog post to a dictionary.""" return { "id": self.id, "title": self.title, "content": self.content, "author": self.author, "tags": self.tags, "published": self.published, "created_at": self.created_at, "updated_at": self.updated_at } @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'BlogPost': """Create a blog post from a dictionary.""" return cls( id=data.get("id"), title=data.get("title", ""), content=data.get("content", ""), author=data.get("author", ""), tags=data.get("tags", []), published=data.get("published", False), created_at=data.get("created_at"), updated_at=data.get("updated_at") ) class BlogOperationResult: """ Model representing the result of a blog operation. """ def __init__(self, success: bool, message: str, data: Any = None): """ Initialize a blog operation result. Args: success: Whether the operation was successful message: A message describing the result data: Optional data returned by the operation """ self.success = success self.message = message self.data = data def to_dict(self) -> Dict[str, Any]: """ Convert the result to a dictionary. Returns: Dictionary representation of the result """ return { "success": self.success, "message": self.message, "data": self.data } class ToolUseRequest: """ Model representing a tool use request from Claude. """ def __init__(self, command: str, **kwargs): """ Initialize a tool use request. Args: command: The command to execute **kwargs: Additional arguments for the command """ self.command = command self.kwargs = kwargs @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'ToolUseRequest': """ Create a tool use request from a dictionary. Args: data: Dictionary containing the tool use request Returns: A ToolUseRequest instance """ command = data.get("command") # Extract all other keys as kwargs kwargs = {k: v for k, v in data.items() if k != "command"} return cls(command, **kwargs) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/blog_agent/read_tool.py ================================================ #!/usr/bin/env python3 """ Read tool for the blog agent in the Vertical Slice Architecture. This module provides blog post reading capabilities. """ import sys import os import json import glob from typing import Dict, Any, List, Optional # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.blog_agent.model_tools import BlogPost, BlogOperationResult # Path to store blog posts BLOG_POSTS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "data", "blog_posts") def read_blog_post(post_id: str) -> BlogOperationResult: """ Read a blog post by ID. Args: post_id: The ID of the blog post to read Returns: BlogOperationResult with the blog post or error message """ log_info("read_tool", f"Reading blog post with ID: {post_id}") try: # Read the blog post from the JSON file file_path = os.path.join(BLOG_POSTS_DIR, f"{post_id}.json") if not os.path.exists(file_path): error_msg = f"Blog post with ID {post_id} not found" log_error("read_tool", error_msg) return BlogOperationResult(success=False, message=error_msg) with open(file_path, 'r', encoding='utf-8') as f: blog_post_data = json.load(f) # Create a BlogPost object from the data blog_post = BlogPost.from_dict(blog_post_data) log_info("read_tool", f"Successfully read blog post: {blog_post.title}") return BlogOperationResult( success=True, message=f"Successfully read blog post: {blog_post.title}", data=blog_post.to_dict() ) except Exception as e: error_msg = f"Failed to read blog post: {str(e)}" log_error("read_tool", error_msg) return BlogOperationResult(success=False, message=error_msg) def list_blog_posts(tag: Optional[str] = None, author: Optional[str] = None, published_only: bool = False) -> BlogOperationResult: """ List all blog posts, optionally filtered by tag, author, or publication status. Args: tag: Optional tag to filter by author: Optional author to filter by published_only: Whether to only return published posts Returns: BlogOperationResult with a list of blog posts or error message """ log_info("read_tool", "Listing blog posts") try: # Create directory if it doesn't exist os.makedirs(BLOG_POSTS_DIR, exist_ok=True) # Get all JSON files in the blog posts directory file_paths = glob.glob(os.path.join(BLOG_POSTS_DIR, "*.json")) blog_posts = [] for file_path in file_paths: try: with open(file_path, 'r', encoding='utf-8') as f: blog_post_data = json.load(f) # Apply filters if published_only and not blog_post_data.get("published", False): continue if author and blog_post_data.get("author") != author: continue if tag and tag not in blog_post_data.get("tags", []): continue blog_posts.append(blog_post_data) except Exception as e: log_error("read_tool", f"Error reading file {file_path}: {str(e)}") continue log_info("read_tool", f"Listed {len(blog_posts)} blog posts") return BlogOperationResult( success=True, message=f"Successfully listed {len(blog_posts)} blog posts", data=blog_posts ) except Exception as e: error_msg = f"Failed to list blog posts: {str(e)}" log_error("read_tool", error_msg) return BlogOperationResult(success=False, message=error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/blog_agent/search_tool.py ================================================ #!/usr/bin/env python3 """ Search tool for the blog agent in the Vertical Slice Architecture. This module provides blog post searching capabilities. """ import sys import os import json import glob import re from typing import Dict, Any, List, Optional # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.blog_agent.model_tools import BlogOperationResult # Path to store blog posts BLOG_POSTS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "data", "blog_posts") def search_blog_posts(query: str, search_content: bool = True, tag: Optional[str] = None, author: Optional[str] = None) -> BlogOperationResult: """ Search blog posts by query string, optionally filtered by tag or author. Args: query: The search query search_content: Whether to search in the content (otherwise just title and tags) tag: Optional tag to filter by author: Optional author to filter by Returns: BlogOperationResult with a list of matching blog posts or error message """ log_info("search_tool", f"Searching blog posts for: {query}") try: # Create directory if it doesn't exist os.makedirs(BLOG_POSTS_DIR, exist_ok=True) # Get all JSON files in the blog posts directory file_paths = glob.glob(os.path.join(BLOG_POSTS_DIR, "*.json")) # Compile the search regex for case-insensitive search search_regex = re.compile(query, re.IGNORECASE) matching_posts = [] for file_path in file_paths: try: with open(file_path, 'r', encoding='utf-8') as f: blog_post_data = json.load(f) # Apply filters if author and blog_post_data.get("author") != author: continue if tag and tag not in blog_post_data.get("tags", []): continue # Check for match in title if search_regex.search(blog_post_data.get("title", "")): matching_posts.append(blog_post_data) continue # Check for match in tags if any(search_regex.search(t) for t in blog_post_data.get("tags", [])): matching_posts.append(blog_post_data) continue # Check for match in content if requested if search_content and search_regex.search(blog_post_data.get("content", "")): matching_posts.append(blog_post_data) continue except Exception as e: log_error("search_tool", f"Error processing file {file_path}: {str(e)}") continue log_info("search_tool", f"Found {len(matching_posts)} matching blog posts") return BlogOperationResult( success=True, message=f"Found {len(matching_posts)} matching blog posts", data=matching_posts ) except Exception as e: error_msg = f"Failed to search blog posts: {str(e)}" log_error("search_tool", error_msg) return BlogOperationResult(success=False, message=error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/blog_agent/tool_handler.py ================================================ #!/usr/bin/env python3 """ Tool handler for the Vertical Slice Architecture implementation of the blog agent. This module handles tool use requests from the Claude agent. """ import sys import os import json from typing import Dict, Any, List, Optional # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.blog_agent.model_tools import ToolUseRequest from features.blog_agent.blog_manager import BlogManager def handle_tool_use(input_data: Dict[str, Any]) -> Dict[str, Any]: """ Handle tool use requests from the Claude agent. Args: input_data: The tool use request data from Claude Returns: Dictionary with the result or error message """ log_info("tool_handler", f"Received tool use request: {input_data}") try: # Parse the tool use request request = ToolUseRequest.from_dict(input_data) # Handle the command if request.command == "create_post": title = request.kwargs.get("title", "") content = request.kwargs.get("content", "") author = request.kwargs.get("author", "") tags = request.kwargs.get("tags", []) result = BlogManager.create_post(title, content, author, tags) elif request.command == "get_post": post_id = request.kwargs.get("post_id", "") result = BlogManager.get_post(post_id) elif request.command == "update_post": post_id = request.kwargs.get("post_id", "") title = request.kwargs.get("title") content = request.kwargs.get("content") tags = request.kwargs.get("tags") published = request.kwargs.get("published") result = BlogManager.update_post(post_id, title, content, tags, published) elif request.command == "delete_post": post_id = request.kwargs.get("post_id", "") result = BlogManager.delete_post(post_id) elif request.command == "list_posts": tag = request.kwargs.get("tag") author = request.kwargs.get("author") published_only = request.kwargs.get("published_only", False) result = BlogManager.list_posts(tag, author, published_only) elif request.command == "search_posts": query = request.kwargs.get("query", "") search_content = request.kwargs.get("search_content", True) tag = request.kwargs.get("tag") author = request.kwargs.get("author") result = BlogManager.search_posts(query, search_content, tag, author) elif request.command == "publish_post": post_id = request.kwargs.get("post_id", "") result = BlogManager.publish_post(post_id) elif request.command == "unpublish_post": post_id = request.kwargs.get("post_id", "") result = BlogManager.unpublish_post(post_id) else: log_error("tool_handler", f"Unknown command: {request.command}") return {"error": f"Unknown command: {request.command}"} # Return the result if result.success: # Convert complex objects to JSON serializable format if isinstance(result.data, dict) or isinstance(result.data, list): # Convert to JSON string and back to ensure serializability clean_data = json.loads(json.dumps(result.data)) return {"result": result.message, "data": clean_data} else: return {"result": result.message} else: return {"error": result.message} except Exception as e: error_msg = f"Error handling tool use: {str(e)}" log_error("tool_handler", error_msg) return {"error": error_msg} ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/blog_agent/update_tool.py ================================================ #!/usr/bin/env python3 """ Update tool for the blog agent in the Vertical Slice Architecture. This module provides blog post updating capabilities. """ import sys import os import json from datetime import datetime from typing import Dict, Any, List, Optional # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.blog_agent.model_tools import BlogPost, BlogOperationResult from features.blog_agent.read_tool import read_blog_post # Path to store blog posts BLOG_POSTS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "data", "blog_posts") def update_blog_post(post_id: str, title: Optional[str] = None, content: Optional[str] = None, tags: Optional[List[str]] = None, published: Optional[bool] = None) -> BlogOperationResult: """ Update a blog post by ID. Args: post_id: The ID of the blog post to update title: Optional new title content: Optional new content tags: Optional new tags published: Optional new publication status Returns: BlogOperationResult with the updated blog post or error message """ log_info("update_tool", f"Updating blog post with ID: {post_id}") try: # Read the existing blog post read_result = read_blog_post(post_id) if not read_result.success: return read_result # Get the existing blog post data blog_post_data = read_result.data # Update the fields if title is not None: blog_post_data["title"] = title if content is not None: blog_post_data["content"] = content if tags is not None: blog_post_data["tags"] = tags if published is not None: blog_post_data["published"] = published # Update the updated_at timestamp blog_post_data["updated_at"] = datetime.now().isoformat() # Save the updated blog post to the JSON file file_path = os.path.join(BLOG_POSTS_DIR, f"{post_id}.json") with open(file_path, 'w', encoding='utf-8') as f: json.dump(blog_post_data, f, indent=2) log_info("update_tool", f"Updated blog post: {blog_post_data['title']}") return BlogOperationResult( success=True, message=f"Successfully updated blog post: {blog_post_data['title']}", data=blog_post_data ) except Exception as e: error_msg = f"Failed to update blog post: {str(e)}" log_error("update_tool", error_msg) return BlogOperationResult(success=False, message=error_msg) def publish_blog_post(post_id: str) -> BlogOperationResult: """ Publish a blog post by ID. Args: post_id: The ID of the blog post to publish Returns: BlogOperationResult with the published blog post or error message """ log_info("update_tool", f"Publishing blog post with ID: {post_id}") return update_blog_post(post_id, published=True) def unpublish_blog_post(post_id: str) -> BlogOperationResult: """ Unpublish a blog post by ID. Args: post_id: The ID of the blog post to unpublish Returns: BlogOperationResult with the unpublished blog post or error message """ log_info("update_tool", f"Unpublishing blog post with ID: {post_id}") return update_blog_post(post_id, published=False) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/blog_agent_v2/__init__.py ================================================ """ Blog agent package for the Vertical Slice Architecture. This package provides blog management capabilities. """ from features.blog_agent.blog_agent import run_agent from features.blog_agent.blog_manager import BlogManager ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/blog_agent_v2/blog_agent.py ================================================ #!/usr/bin/env python3 """ Blog agent for the Vertical Slice Architecture implementation of the blog agent. This module provides the agent interface for blog management operations. """ import time from typing import Tuple, Dict, Any, List, Optional from rich.console import Console from rich.panel import Panel from rich.markdown import Markdown from anthropic import Anthropic import sys import os # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error, display_token_usage from features.blog_agent.tool_handler import handle_tool_use # Initialize rich console console = Console() # Define constants MODEL = "claude-3-7-sonnet-20250219" DEFAULT_THINKING_TOKENS = 3000 class BlogAgent: """ Blog agent that provides an interface for AI-assisted blog management. """ @staticmethod def run_agent( client: Anthropic, prompt: str, max_thinking_tokens: int = DEFAULT_THINKING_TOKENS, max_loops: int = 10, use_token_efficiency: bool = False, ) -> Tuple[str, int, int]: """ Run the Claude agent with blog management capabilities. Args: client: The Anthropic client prompt: The user's prompt max_thinking_tokens: Maximum tokens for thinking max_loops: Maximum number of tool use loops use_token_efficiency: Whether to use token-efficient tool use beta feature Returns: Tuple containing: - Final response from Claude (str) - Total input tokens used (int) - Total output tokens used (int) """ # Track token usage input_tokens_total = 0 output_tokens_total = 0 system_prompt = """You are a helpful AI assistant with blog management capabilities. You have access to tools that can create, read, update, delete, and search blog posts. Always think step by step about what you need to do before taking any action. Be helpful in suggesting blog post ideas and improvements when asked. Available commands: - create_post: Create a new blog post (title, content, author, tags) - get_post: Get a blog post by ID (post_id) - update_post: Update a blog post (post_id, title?, content?, tags?, published?) - delete_post: Delete a blog post (post_id) - list_posts: List blog posts (tag?, author?, published_only?) - search_posts: Search blog posts (query, search_content?, tag?, author?) - publish_post: Publish a blog post (post_id) - unpublish_post: Unpublish a blog post (post_id) """ # Define blog management tool blog_management_tool = { "name": "blog_management", "description": "Manage blog posts including creation, editing, searching, and publishing", "input_schema": { "type": "object", "properties": { "command": { "type": "string", "enum": [ "create_post", "get_post", "update_post", "delete_post", "list_posts", "search_posts", "publish_post", "unpublish_post" ], "description": "The blog management command to execute" } }, "required": ["command"] } } messages = [ { "role": "user", "content": f"""I need help managing my blog. Here's what I want to do: {prompt} Please use the blog management tools to help me with this. First, think through what you need to do, then use the appropriate tools. """, } ] loop_count = 0 tool_use_count = 0 thinking_start_time = time.time() while loop_count < max_loops: loop_count += 1 console.rule(f"[yellow]Agent Loop {loop_count}/{max_loops}[/yellow]") log_info("blog_agent", f"Starting agent loop {loop_count}/{max_loops}") # Create message with blog management tool message_args = { "model": MODEL, "max_tokens": 4096, "tools": [blog_management_tool], "messages": messages, "system": system_prompt, "thinking": {"type": "enabled", "budget_tokens": max_thinking_tokens}, } # Use the beta.messages with betas parameter if token efficiency is enabled if use_token_efficiency: # Using token-efficient tools beta feature message_args["betas"] = ["token-efficient-tools-2025-02-19"] response = client.beta.messages.create(**message_args) else: # Standard approach response = client.messages.create(**message_args) # Track token usage if hasattr(response, "usage"): input_tokens = getattr(response.usage, "input_tokens", 0) output_tokens = getattr(response.usage, "output_tokens", 0) input_tokens_total += input_tokens output_tokens_total += output_tokens console.print( f"[dim]Loop {loop_count} tokens: Input={input_tokens}, Output={output_tokens}[/dim]" ) log_info( "blog_agent", f"Loop {loop_count} tokens: Input={input_tokens}, Output={output_tokens}" ) # Process response content thinking_block = None tool_use_block = None text_block = None for content_block in response.content: if content_block.type == "thinking": thinking_block = content_block # Access the thinking attribute which contains the actual thinking text if hasattr(thinking_block, "thinking"): console.print( Panel( thinking_block.thinking, title=f"Claude's Thinking (Loop {loop_count})", border_style="blue", ) ) else: console.print( Panel( "Claude is thinking...", title=f"Claude's Thinking (Loop {loop_count})", border_style="blue", ) ) elif content_block.type == "tool_use": tool_use_block = content_block tool_use_count += 1 elif content_block.type == "text": text_block = content_block # If we got a final text response with no tool use, we're done if text_block and not tool_use_block: thinking_end_time = time.time() thinking_duration = thinking_end_time - thinking_start_time console.print( f"\n[bold green]Completed in {thinking_duration:.2f} seconds after {loop_count} loops and {tool_use_count} tool uses[/bold green]" ) log_info( "blog_agent", f"Completed in {thinking_duration:.2f} seconds after {loop_count} loops and {tool_use_count} tool uses" ) # Add the response to messages messages.append( { "role": "assistant", "content": [ *([thinking_block] if thinking_block else []), {"type": "text", "text": text_block.text}, ], } ) return text_block.text, input_tokens_total, output_tokens_total # Handle tool use if tool_use_block: # Add the assistant's response to messages before handling tool calls messages.append({"role": "assistant", "content": response.content}) console.print( f"\n[bold blue]Tool Call:[/bold blue] {tool_use_block.name}" ) log_info("blog_agent", f"Tool Call: {tool_use_block.name}") # Handle the tool use tool_result = handle_tool_use(tool_use_block.input) # Format tool result for Claude tool_result_message = { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_use_block.id, "content": tool_result.get("error") or tool_result.get("result", ""), } ], } # If we have data in the result, include it as formatted markdown if "data" in tool_result and tool_result["data"]: data_json = json.dumps(tool_result["data"], indent=2) tool_result_message["content"][0]["content"] += f"\n\n```json\n{data_json}\n```" messages.append(tool_result_message) # If we reach here, we hit the max loops console.print( f"\n[bold red]Warning: Reached maximum loops ({max_loops}) without completing the task[/bold red]" ) log_error( "blog_agent", f"Reached maximum loops ({max_loops}) without completing the task" ) return ( "I wasn't able to complete the task within the allowed number of thinking steps. Please try a more specific prompt or increase the loop limit.", input_tokens_total, output_tokens_total, ) # Expose the run_agent function at the module level def run_agent( client: Anthropic, prompt: str, max_tool_use_loops: int = 15, token_efficient_tool_use: bool = True, ) -> Tuple[int, int]: """ Run the blog agent with the specified prompt. Args: client: The Anthropic client prompt: The prompt to send to Claude max_tool_use_loops: Maximum number of tool use loops token_efficient_tool_use: Whether to use token-efficient tool use Returns: Tuple containing input and output token counts """ log_info("blog_agent", f"Running agent with prompt: {prompt}") _, input_tokens, output_tokens = BlogAgent.run_agent( client=client, prompt=prompt, max_loops=max_tool_use_loops, use_token_efficiency=token_efficient_tool_use, max_thinking_tokens=DEFAULT_THINKING_TOKENS ) return input_tokens, output_tokens ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/blog_agent_v2/blog_manager.py ================================================ #!/usr/bin/env python3 """ Blog manager for the Vertical Slice Architecture implementation of the blog agent. This module combines various blog tools to provide comprehensive blog management capabilities. """ import sys import os from typing import Dict, Any, List, Optional # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.blog_agent.model_tools import BlogOperationResult from features.blog_agent.create_tool import create_blog_post from features.blog_agent.read_tool import read_blog_post, list_blog_posts from features.blog_agent.update_tool import update_blog_post, publish_blog_post, unpublish_blog_post from features.blog_agent.delete_tool import delete_blog_post from features.blog_agent.search_tool import search_blog_posts class BlogManager: """ Blog manager that combines various tools to provide blog management capabilities. """ @staticmethod def create_post(title: str, content: str, author: str, tags: List[str] = None) -> BlogOperationResult: """ Create a new blog post. Args: title: The title of the blog post content: The content of the blog post author: The author of the blog post tags: Optional list of tags Returns: BlogOperationResult with result or error message """ log_info("blog_manager", f"Creating blog post: {title}") return create_blog_post(title, content, author, tags) @staticmethod def get_post(post_id: str) -> BlogOperationResult: """ Get a blog post by ID. Args: post_id: The ID of the blog post to get Returns: BlogOperationResult with the blog post or error message """ log_info("blog_manager", f"Getting blog post: {post_id}") return read_blog_post(post_id) @staticmethod def update_post(post_id: str, title: Optional[str] = None, content: Optional[str] = None, tags: Optional[List[str]] = None, published: Optional[bool] = None) -> BlogOperationResult: """ Update a blog post. Args: post_id: The ID of the blog post to update title: Optional new title content: Optional new content tags: Optional new tags published: Optional new publication status Returns: BlogOperationResult with the updated blog post or error message """ log_info("blog_manager", f"Updating blog post: {post_id}") return update_blog_post(post_id, title, content, tags, published) @staticmethod def delete_post(post_id: str) -> BlogOperationResult: """ Delete a blog post. Args: post_id: The ID of the blog post to delete Returns: BlogOperationResult with result or error message """ log_info("blog_manager", f"Deleting blog post: {post_id}") return delete_blog_post(post_id) @staticmethod def list_posts(tag: Optional[str] = None, author: Optional[str] = None, published_only: bool = False) -> BlogOperationResult: """ List blog posts, optionally filtered by tag, author, or publication status. Args: tag: Optional tag to filter by author: Optional author to filter by published_only: Whether to only return published posts Returns: BlogOperationResult with a list of blog posts or error message """ log_info("blog_manager", "Listing blog posts") return list_blog_posts(tag, author, published_only) @staticmethod def search_posts(query: str, search_content: bool = True, tag: Optional[str] = None, author: Optional[str] = None) -> BlogOperationResult: """ Search blog posts by query string, optionally filtered by tag or author. Args: query: The search query search_content: Whether to search in the content (otherwise just title and tags) tag: Optional tag to filter by author: Optional author to filter by Returns: BlogOperationResult with a list of matching blog posts or error message """ log_info("blog_manager", f"Searching blog posts for: {query}") return search_blog_posts(query, search_content, tag, author) @staticmethod def publish_post(post_id: str) -> BlogOperationResult: """ Publish a blog post. Args: post_id: The ID of the blog post to publish Returns: BlogOperationResult with the published blog post or error message """ log_info("blog_manager", f"Publishing blog post: {post_id}") return publish_blog_post(post_id) @staticmethod def unpublish_post(post_id: str) -> BlogOperationResult: """ Unpublish a blog post. Args: post_id: The ID of the blog post to unpublish Returns: BlogOperationResult with the unpublished blog post or error message """ log_info("blog_manager", f"Unpublishing blog post: {post_id}") return unpublish_blog_post(post_id) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/blog_agent_v2/create_tool.py ================================================ #!/usr/bin/env python3 """ Create tool for the blog agent in the Vertical Slice Architecture. This module provides blog post creation capabilities. """ import sys import os import json import uuid from datetime import datetime from typing import Dict, Any # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.blog_agent.model_tools import BlogPost, BlogOperationResult # Path to store blog posts BLOG_POSTS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "data", "blog_posts") def create_blog_post(title: str, content: str, author: str, tags: list = None) -> BlogOperationResult: """ Create a new blog post. Args: title: Title of the blog post content: Content of the blog post author: Author of the blog post tags: Optional list of tags Returns: BlogOperationResult with result or error message """ log_info("create_tool", f"Creating blog post: {title}") try: # Create directory if it doesn't exist os.makedirs(BLOG_POSTS_DIR, exist_ok=True) # Generate a unique ID and timestamps post_id = str(uuid.uuid4()) current_time = datetime.now().isoformat() # Create the blog post blog_post = BlogPost( id=post_id, title=title, content=content, author=author, tags=tags or [], published=False, created_at=current_time, updated_at=current_time ) # Save the blog post to a JSON file file_path = os.path.join(BLOG_POSTS_DIR, f"{post_id}.json") with open(file_path, 'w', encoding='utf-8') as f: json.dump(blog_post.to_dict(), f, indent=2) log_info("create_tool", f"Created blog post: {title} with ID: {post_id}") return BlogOperationResult( success=True, message=f"Successfully created blog post: {title}", data=blog_post.to_dict() ) except Exception as e: error_msg = f"Failed to create blog post: {str(e)}" log_error("create_tool", error_msg) return BlogOperationResult(success=False, message=error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/blog_agent_v2/delete_tool.py ================================================ #!/usr/bin/env python3 """ Delete tool for the blog agent in the Vertical Slice Architecture. This module provides blog post deletion capabilities. """ import sys import os import json from typing import Dict, Any # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.blog_agent.model_tools import BlogOperationResult from features.blog_agent.read_tool import read_blog_post # Path to store blog posts BLOG_POSTS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "data", "blog_posts") def delete_blog_post(post_id: str) -> BlogOperationResult: """ Delete a blog post by ID. Args: post_id: The ID of the blog post to delete Returns: BlogOperationResult with result or error message """ log_info("delete_tool", f"Deleting blog post with ID: {post_id}") try: # Verify the blog post exists read_result = read_blog_post(post_id) if not read_result.success: return read_result # Get the blog post title for the response message blog_post_title = read_result.data["title"] # Delete the blog post file file_path = os.path.join(BLOG_POSTS_DIR, f"{post_id}.json") os.remove(file_path) log_info("delete_tool", f"Deleted blog post: {blog_post_title}") return BlogOperationResult( success=True, message=f"Successfully deleted blog post: {blog_post_title}" ) except Exception as e: error_msg = f"Failed to delete blog post: {str(e)}" log_error("delete_tool", error_msg) return BlogOperationResult(success=False, message=error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/blog_agent_v2/model_tools.py ================================================ #!/usr/bin/env python3 """ Models for the blog agent in the Vertical Slice Architecture. """ import os import sys from typing import Dict, Any, Optional, List, Union from dataclasses import dataclass # Add the project root to the Python path sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../'))) @dataclass class BlogPost: """Model representing a blog post.""" title: str content: str author: str tags: List[str] published: bool = False id: Optional[str] = None created_at: Optional[str] = None updated_at: Optional[str] = None def to_dict(self) -> Dict[str, Any]: """Convert the blog post to a dictionary.""" return { "id": self.id, "title": self.title, "content": self.content, "author": self.author, "tags": self.tags, "published": self.published, "created_at": self.created_at, "updated_at": self.updated_at } @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'BlogPost': """Create a blog post from a dictionary.""" return cls( id=data.get("id"), title=data.get("title", ""), content=data.get("content", ""), author=data.get("author", ""), tags=data.get("tags", []), published=data.get("published", False), created_at=data.get("created_at"), updated_at=data.get("updated_at") ) class BlogOperationResult: """ Model representing the result of a blog operation. """ def __init__(self, success: bool, message: str, data: Any = None): """ Initialize a blog operation result. Args: success: Whether the operation was successful message: A message describing the result data: Optional data returned by the operation """ self.success = success self.message = message self.data = data def to_dict(self) -> Dict[str, Any]: """ Convert the result to a dictionary. Returns: Dictionary representation of the result """ return { "success": self.success, "message": self.message, "data": self.data } class ToolUseRequest: """ Model representing a tool use request from Claude. """ def __init__(self, command: str, **kwargs): """ Initialize a tool use request. Args: command: The command to execute **kwargs: Additional arguments for the command """ self.command = command self.kwargs = kwargs @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'ToolUseRequest': """ Create a tool use request from a dictionary. Args: data: Dictionary containing the tool use request Returns: A ToolUseRequest instance """ command = data.get("command") # Extract all other keys as kwargs kwargs = {k: v for k, v in data.items() if k != "command"} return cls(command, **kwargs) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/blog_agent_v2/read_tool.py ================================================ #!/usr/bin/env python3 """ Read tool for the blog agent in the Vertical Slice Architecture. This module provides blog post reading capabilities. """ import sys import os import json import glob from typing import Dict, Any, List, Optional # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.blog_agent.model_tools import BlogPost, BlogOperationResult # Path to store blog posts BLOG_POSTS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "data", "blog_posts") def read_blog_post(post_id: str) -> BlogOperationResult: """ Read a blog post by ID. Args: post_id: The ID of the blog post to read Returns: BlogOperationResult with the blog post or error message """ log_info("read_tool", f"Reading blog post with ID: {post_id}") try: # Read the blog post from the JSON file file_path = os.path.join(BLOG_POSTS_DIR, f"{post_id}.json") if not os.path.exists(file_path): error_msg = f"Blog post with ID {post_id} not found" log_error("read_tool", error_msg) return BlogOperationResult(success=False, message=error_msg) with open(file_path, 'r', encoding='utf-8') as f: blog_post_data = json.load(f) # Create a BlogPost object from the data blog_post = BlogPost.from_dict(blog_post_data) log_info("read_tool", f"Successfully read blog post: {blog_post.title}") return BlogOperationResult( success=True, message=f"Successfully read blog post: {blog_post.title}", data=blog_post.to_dict() ) except Exception as e: error_msg = f"Failed to read blog post: {str(e)}" log_error("read_tool", error_msg) return BlogOperationResult(success=False, message=error_msg) def list_blog_posts(tag: Optional[str] = None, author: Optional[str] = None, published_only: bool = False) -> BlogOperationResult: """ List all blog posts, optionally filtered by tag, author, or publication status. Args: tag: Optional tag to filter by author: Optional author to filter by published_only: Whether to only return published posts Returns: BlogOperationResult with a list of blog posts or error message """ log_info("read_tool", "Listing blog posts") try: # Create directory if it doesn't exist os.makedirs(BLOG_POSTS_DIR, exist_ok=True) # Get all JSON files in the blog posts directory file_paths = glob.glob(os.path.join(BLOG_POSTS_DIR, "*.json")) blog_posts = [] for file_path in file_paths: try: with open(file_path, 'r', encoding='utf-8') as f: blog_post_data = json.load(f) # Apply filters if published_only and not blog_post_data.get("published", False): continue if author and blog_post_data.get("author") != author: continue if tag and tag not in blog_post_data.get("tags", []): continue blog_posts.append(blog_post_data) except Exception as e: log_error("read_tool", f"Error reading file {file_path}: {str(e)}") continue log_info("read_tool", f"Listed {len(blog_posts)} blog posts") return BlogOperationResult( success=True, message=f"Successfully listed {len(blog_posts)} blog posts", data=blog_posts ) except Exception as e: error_msg = f"Failed to list blog posts: {str(e)}" log_error("read_tool", error_msg) return BlogOperationResult(success=False, message=error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/blog_agent_v2/search_tool.py ================================================ #!/usr/bin/env python3 """ Search tool for the blog agent in the Vertical Slice Architecture. This module provides blog post searching capabilities. """ import sys import os import json import glob import re from typing import Dict, Any, List, Optional # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.blog_agent.model_tools import BlogOperationResult # Path to store blog posts BLOG_POSTS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "data", "blog_posts") def search_blog_posts(query: str, search_content: bool = True, tag: Optional[str] = None, author: Optional[str] = None) -> BlogOperationResult: """ Search blog posts by query string, optionally filtered by tag or author. Args: query: The search query search_content: Whether to search in the content (otherwise just title and tags) tag: Optional tag to filter by author: Optional author to filter by Returns: BlogOperationResult with a list of matching blog posts or error message """ log_info("search_tool", f"Searching blog posts for: {query}") try: # Create directory if it doesn't exist os.makedirs(BLOG_POSTS_DIR, exist_ok=True) # Get all JSON files in the blog posts directory file_paths = glob.glob(os.path.join(BLOG_POSTS_DIR, "*.json")) # Compile the search regex for case-insensitive search search_regex = re.compile(query, re.IGNORECASE) matching_posts = [] for file_path in file_paths: try: with open(file_path, 'r', encoding='utf-8') as f: blog_post_data = json.load(f) # Apply filters if author and blog_post_data.get("author") != author: continue if tag and tag not in blog_post_data.get("tags", []): continue # Check for match in title if search_regex.search(blog_post_data.get("title", "")): matching_posts.append(blog_post_data) continue # Check for match in tags if any(search_regex.search(t) for t in blog_post_data.get("tags", [])): matching_posts.append(blog_post_data) continue # Check for match in content if requested if search_content and search_regex.search(blog_post_data.get("content", "")): matching_posts.append(blog_post_data) continue except Exception as e: log_error("search_tool", f"Error processing file {file_path}: {str(e)}") continue log_info("search_tool", f"Found {len(matching_posts)} matching blog posts") return BlogOperationResult( success=True, message=f"Found {len(matching_posts)} matching blog posts", data=matching_posts ) except Exception as e: error_msg = f"Failed to search blog posts: {str(e)}" log_error("search_tool", error_msg) return BlogOperationResult(success=False, message=error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/blog_agent_v2/tool_handler.py ================================================ #!/usr/bin/env python3 """ Tool handler for the Vertical Slice Architecture implementation of the blog agent. This module handles tool use requests from the Claude agent. """ import sys import os import json from typing import Dict, Any, List, Optional # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.blog_agent.model_tools import ToolUseRequest from features.blog_agent.blog_manager import BlogManager def handle_tool_use(input_data: Dict[str, Any]) -> Dict[str, Any]: """ Handle tool use requests from the Claude agent. Args: input_data: The tool use request data from Claude Returns: Dictionary with the result or error message """ log_info("tool_handler", f"Received tool use request: {input_data}") try: # Parse the tool use request request = ToolUseRequest.from_dict(input_data) # Handle the command if request.command == "create_post": title = request.kwargs.get("title", "") content = request.kwargs.get("content", "") author = request.kwargs.get("author", "") tags = request.kwargs.get("tags", []) result = BlogManager.create_post(title, content, author, tags) elif request.command == "get_post": post_id = request.kwargs.get("post_id", "") result = BlogManager.get_post(post_id) elif request.command == "update_post": post_id = request.kwargs.get("post_id", "") title = request.kwargs.get("title") content = request.kwargs.get("content") tags = request.kwargs.get("tags") published = request.kwargs.get("published") result = BlogManager.update_post(post_id, title, content, tags, published) elif request.command == "delete_post": post_id = request.kwargs.get("post_id", "") result = BlogManager.delete_post(post_id) elif request.command == "list_posts": tag = request.kwargs.get("tag") author = request.kwargs.get("author") published_only = request.kwargs.get("published_only", False) result = BlogManager.list_posts(tag, author, published_only) elif request.command == "search_posts": query = request.kwargs.get("query", "") search_content = request.kwargs.get("search_content", True) tag = request.kwargs.get("tag") author = request.kwargs.get("author") result = BlogManager.search_posts(query, search_content, tag, author) elif request.command == "publish_post": post_id = request.kwargs.get("post_id", "") result = BlogManager.publish_post(post_id) elif request.command == "unpublish_post": post_id = request.kwargs.get("post_id", "") result = BlogManager.unpublish_post(post_id) else: log_error("tool_handler", f"Unknown command: {request.command}") return {"error": f"Unknown command: {request.command}"} # Return the result if result.success: # Convert complex objects to JSON serializable format if isinstance(result.data, dict) or isinstance(result.data, list): # Convert to JSON string and back to ensure serializability clean_data = json.loads(json.dumps(result.data)) return {"result": result.message, "data": clean_data} else: return {"result": result.message} else: return {"error": result.message} except Exception as e: error_msg = f"Error handling tool use: {str(e)}" log_error("tool_handler", error_msg) return {"error": error_msg} ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/blog_agent_v2/update_tool.py ================================================ #!/usr/bin/env python3 """ Update tool for the blog agent in the Vertical Slice Architecture. This module provides blog post updating capabilities. """ import sys import os import json from datetime import datetime from typing import Dict, Any, List, Optional # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.blog_agent.model_tools import BlogPost, BlogOperationResult from features.blog_agent.read_tool import read_blog_post # Path to store blog posts BLOG_POSTS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "data", "blog_posts") def update_blog_post(post_id: str, title: Optional[str] = None, content: Optional[str] = None, tags: Optional[List[str]] = None, published: Optional[bool] = None) -> BlogOperationResult: """ Update a blog post by ID. Args: post_id: The ID of the blog post to update title: Optional new title content: Optional new content tags: Optional new tags published: Optional new publication status Returns: BlogOperationResult with the updated blog post or error message """ log_info("update_tool", f"Updating blog post with ID: {post_id}") try: # Read the existing blog post read_result = read_blog_post(post_id) if not read_result.success: return read_result # Get the existing blog post data blog_post_data = read_result.data # Update the fields if title is not None: blog_post_data["title"] = title if content is not None: blog_post_data["content"] = content if tags is not None: blog_post_data["tags"] = tags if published is not None: blog_post_data["published"] = published # Update the updated_at timestamp blog_post_data["updated_at"] = datetime.now().isoformat() # Save the updated blog post to the JSON file file_path = os.path.join(BLOG_POSTS_DIR, f"{post_id}.json") with open(file_path, 'w', encoding='utf-8') as f: json.dump(blog_post_data, f, indent=2) log_info("update_tool", f"Updated blog post: {blog_post_data['title']}") return BlogOperationResult( success=True, message=f"Successfully updated blog post: {blog_post_data['title']}", data=blog_post_data ) except Exception as e: error_msg = f"Failed to update blog post: {str(e)}" log_error("update_tool", error_msg) return BlogOperationResult(success=False, message=error_msg) def publish_blog_post(post_id: str) -> BlogOperationResult: """ Publish a blog post by ID. Args: post_id: The ID of the blog post to publish Returns: BlogOperationResult with the published blog post or error message """ log_info("update_tool", f"Publishing blog post with ID: {post_id}") return update_blog_post(post_id, published=True) def unpublish_blog_post(post_id: str) -> BlogOperationResult: """ Unpublish a blog post by ID. Args: post_id: The ID of the blog post to unpublish Returns: BlogOperationResult with the unpublished blog post or error message """ log_info("update_tool", f"Unpublishing blog post with ID: {post_id}") return update_blog_post(post_id, published=False) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent/__init__.py ================================================ ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent/api_tools.py ================================================ #!/usr/bin/env python3 """ API layer for file operations in the Vertical Slice Architecture. """ import os import sys import traceback from typing import Dict, Any, Optional, List, Union # Add the project root to the Python path sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../'))) from shared.utils import console from features.file_operations.service import FileOperationService from features.file_operations.model import ToolUseRequest, FileOperationResult class FileOperationsAPI: """ API for file operations. """ @staticmethod def handle_tool_use(tool_use: Dict[str, Any]) -> Dict[str, Any]: """ Handle text editor tool use from Claude. Args: tool_use: The tool use request from Claude Returns: Dictionary with result or error to send back to Claude """ try: # Convert the tool use dictionary to a ToolUseRequest object request = ToolUseRequest.from_dict(tool_use) console.log(f"[handle_tool_use] Received command: {request.command}, path: {request.path}") if not request.command: error_msg = "No command specified in tool use request" console.log(f"[handle_tool_use] Error: {error_msg}") return {"error": error_msg} if not request.path and request.command != "undo_edit": # undo_edit might not need a path error_msg = "No path specified in tool use request" console.log(f"[handle_tool_use] Error: {error_msg}") return {"error": error_msg} # The path normalization is now handled in each file operation function console.print(f"[blue]Executing {request.command} command on {request.path}[/blue]") result = None if request.command == "view": view_range = request.kwargs.get("view_range") console.log( f"[handle_tool_use] Calling view_file with view_range: {view_range}" ) result = FileOperationService.view_file(request.path, view_range) elif request.command == "str_replace": old_str = request.kwargs.get("old_str") new_str = request.kwargs.get("new_str") console.log(f"[handle_tool_use] Calling str_replace") result = FileOperationService.str_replace(request.path, old_str, new_str) elif request.command == "create": file_text = request.kwargs.get("file_text") console.log(f"[handle_tool_use] Calling create_file") result = FileOperationService.create_file(request.path, file_text) elif request.command == "insert": insert_line = request.kwargs.get("insert_line") new_str = request.kwargs.get("new_str") console.log(f"[handle_tool_use] Calling insert_text at line: {insert_line}") result = FileOperationService.insert_text(request.path, insert_line, new_str) elif request.command == "undo_edit": console.log(f"[handle_tool_use] Calling undo_edit") result = FileOperationService.undo_edit(request.path) else: error_msg = f"Unknown command: {request.command}" console.print(f"[red]{error_msg}[/red]") console.log(f"[handle_tool_use] Error: {error_msg}") return {"error": error_msg} # Convert the result to a dictionary if result.success: return {"result": result.data if result.data is not None else result.message} else: return {"error": result.message} except Exception as e: error_msg = f"Error handling tool use: {str(e)}" console.print(f"[red]{error_msg}[/red]") console.log(f"[handle_tool_use] Error: {str(e)}") console.log(traceback.format_exc()) return {"error": error_msg} ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent/create_tool.py ================================================ #!/usr/bin/env python3 """ Create tool for the Vertical Slice Architecture implementation of the file editor agent. This module provides file creation capabilities. """ import sys import os # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.file_operations.model_tools import FileOperationResult from features.file_operations.write_tool import write_file def create_file(path: str, content: str) -> FileOperationResult: """ Create a new file with the specified content. Args: path: The path to the file to create content: The content to write to the file Returns: FileOperationResult with result or error message """ log_info("create_tool", f"Creating file {path}") try: # Create directory if it doesn't exist os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True) # Use the write_file function to create the file return write_file(path, content) except Exception as e: error_msg = f"Failed to create file {path}: {str(e)}" log_error("create_tool", error_msg) return FileOperationResult(success=False, content="", message=error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent/file_agent.py ================================================ #!/usr/bin/env python3 """ File agent for the Vertical Slice Architecture implementation of the file editor agent. This module provides the agent interface for file operations. """ import time from typing import Tuple, Dict, Any, List, Optional, Callable from rich.console import Console from rich.panel import Panel from anthropic import Anthropic import sys import os # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error, display_token_usage from features.file_operations.model_tools import FileOperationResult from features.file_operations.tool_handler import handle_tool_use # Initialize rich console console = Console() # Define constants MODEL = "claude-3-7-sonnet-20250219" DEFAULT_THINKING_TOKENS = 3000 class FileAgent: """ File agent that provides an interface for AI-assisted file operations. """ @staticmethod def run_agent( client: Anthropic, prompt: str, max_thinking_tokens: int = DEFAULT_THINKING_TOKENS, max_loops: int = 10, use_token_efficiency: bool = False, ) -> Tuple[str, int, int]: """ Run the Claude agent with file editing capabilities. Args: client: The Anthropic client prompt: The user's prompt max_thinking_tokens: Maximum tokens for thinking max_loops: Maximum number of tool use loops use_token_efficiency: Whether to use token-efficient tool use beta feature Returns: Tuple containing: - Final response from Claude (str) - Total input tokens used (int) - Total output tokens used (int) """ # Track token usage input_tokens_total = 0 output_tokens_total = 0 system_prompt = """You are a helpful AI assistant with text editing capabilities. You have access to a text editor tool that can view, edit, and create files. Always think step by step about what you need to do before taking any action. Be careful when making edits to files, as they can permanently change the user's files. Follow these steps when handling file operations: 1. First, view files to understand their content before making changes 2. For edits, ensure you have the correct context and are making the right changes 3. When creating files, make sure they're in the right location with proper formatting """ # Define text editor tool text_editor_tool = {"name": "str_replace_editor", "type": "text_editor_20250124"} messages = [ { "role": "user", "content": f"""I need help with editing files. Here's what I want to do: {prompt} Please use the text editor tool to help me with this. First, think through what you need to do, then use the appropriate tool. """, } ] loop_count = 0 tool_use_count = 0 thinking_start_time = time.time() while loop_count < max_loops: loop_count += 1 console.rule(f"[yellow]Agent Loop {loop_count}/{max_loops}[/yellow]") log_info("file_agent", f"Starting agent loop {loop_count}/{max_loops}") # Create message with text editor tool message_args = { "model": MODEL, "max_tokens": 4096, "tools": [text_editor_tool], "messages": messages, "system": system_prompt, "thinking": {"type": "enabled", "budget_tokens": max_thinking_tokens}, } # Use the beta.messages with betas parameter if token efficiency is enabled if use_token_efficiency: # Using token-efficient tools beta feature message_args["betas"] = ["token-efficient-tools-2025-02-19"] response = client.beta.messages.create(**message_args) else: # Standard approach response = client.messages.create(**message_args) # Track token usage if hasattr(response, "usage"): input_tokens = getattr(response.usage, "input_tokens", 0) output_tokens = getattr(response.usage, "output_tokens", 0) input_tokens_total += input_tokens output_tokens_total += output_tokens console.print( f"[dim]Loop {loop_count} tokens: Input={input_tokens}, Output={output_tokens}[/dim]" ) log_info( "file_agent", f"Loop {loop_count} tokens: Input={input_tokens}, Output={output_tokens}" ) # Process response content thinking_block = None tool_use_block = None text_block = None for content_block in response.content: if content_block.type == "thinking": thinking_block = content_block # Access the thinking attribute which contains the actual thinking text if hasattr(thinking_block, "thinking"): console.print( Panel( thinking_block.thinking, title=f"Claude's Thinking (Loop {loop_count})", border_style="blue", ) ) else: console.print( Panel( "Claude is thinking...", title=f"Claude's Thinking (Loop {loop_count})", border_style="blue", ) ) elif content_block.type == "tool_use": tool_use_block = content_block tool_use_count += 1 elif content_block.type == "text": text_block = content_block # If we got a final text response with no tool use, we're done if text_block and not tool_use_block: thinking_end_time = time.time() thinking_duration = thinking_end_time - thinking_start_time console.print( f"\n[bold green]Completed in {thinking_duration:.2f} seconds after {loop_count} loops and {tool_use_count} tool uses[/bold green]" ) log_info( "file_agent", f"Completed in {thinking_duration:.2f} seconds after {loop_count} loops and {tool_use_count} tool uses" ) # Add the response to messages messages.append( { "role": "assistant", "content": [ *([thinking_block] if thinking_block else []), {"type": "text", "text": text_block.text}, ], } ) return text_block.text, input_tokens_total, output_tokens_total # Handle tool use if tool_use_block: # Add the assistant's response to messages before handling tool calls messages.append({"role": "assistant", "content": response.content}) console.print( f"\n[bold blue]Tool Call:[/bold blue] {tool_use_block.name}" ) log_info("file_agent", f"Tool Call: {tool_use_block.name}") # Handle the tool use with our handler tool_result = handle_tool_use(tool_use_block.input) # Format tool result for Claude tool_result_message = { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_use_block.id, "content": tool_result.get("error") or tool_result.get("result", ""), } ], } messages.append(tool_result_message) # If we reach here, we hit the max loops console.print( f"\n[bold red]Warning: Reached maximum loops ({max_loops}) without completing the task[/bold red]" ) log_error( "file_agent", f"Reached maximum loops ({max_loops}) without completing the task" ) return ( "I wasn't able to complete the task within the allowed number of thinking steps. Please try a more specific prompt or increase the loop limit.", input_tokens_total, output_tokens_total, ) # Expose the run_agent function at the module level def run_agent( client: Anthropic, prompt: str, max_tool_use_loops: int = 15, token_efficient_tool_use: bool = True, ) -> Tuple[int, int]: """ Run the file editor agent with the specified prompt. Args: client: The Anthropic client prompt: The prompt to send to Claude max_tool_use_loops: Maximum number of tool use loops token_efficient_tool_use: Whether to use token-efficient tool use Returns: Tuple containing input and output token counts """ log_info("file_agent", f"Running agent with prompt: {prompt}") _, input_tokens, output_tokens = FileAgent.run_agent( client=client, prompt=prompt, max_loops=max_tool_use_loops, use_token_efficiency=token_efficient_tool_use, max_thinking_tokens=DEFAULT_THINKING_TOKENS ) return input_tokens, output_tokens ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent/file_editor.py ================================================ #!/usr/bin/env python3 """ File editor for the Vertical Slice Architecture implementation of the file editor agent. This module combines reading and writing capabilities for file editing. """ import sys import os from typing import Dict, Any, Tuple, Optional # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.file_operations.model_tools import FileOperationResult from features.file_operations.file_writer import FileWriter from features.file_operations.read_tool import read_file class FileEditor: """ File editor that combines reading and writing capabilities for file editing. """ @staticmethod def read(path: str, start_line: Optional[int] = None, end_line: Optional[int] = None) -> FileOperationResult: """ Read the contents of a file. Args: path: The path to the file to read start_line: Optional start line (1-indexed) end_line: Optional end line (1-indexed, -1 for end of file) Returns: FileOperationResult with content or error message """ log_info("file_editor", f"Reading file {path} with range {start_line}-{end_line}") return read_file(path, start_line, end_line) @staticmethod def view_file(path: str, view_range=None) -> FileOperationResult: """ View the contents of a file with optional range. Args: path: The path to the file to view view_range: Optional tuple of (start_line, end_line) Returns: FileOperationResult with content or error message """ start_line = None end_line = None if view_range: start_line, end_line = view_range log_info("file_editor", f"Viewing file {path} with range {start_line}-{end_line}") return FileEditor.read(path, start_line, end_line) @staticmethod def edit_file(path: str, old_str: str, new_str: str) -> FileOperationResult: """ Edit a file by replacing one string with another. Args: path: The path to the file to edit old_str: The string to replace new_str: The string to replace it with Returns: FileOperationResult with result or error message """ log_info("file_editor", f"Editing file {path}") # First, read the file to check if it exists read_result = FileEditor.read(path) if not read_result.success: log_error("file_editor", f"Cannot edit file that can't be read: {read_result.message}") return read_result # Then, use the file writer to replace the string return FileWriter.replace(path, old_str, new_str) @staticmethod def create_file(path: str, content: str) -> FileOperationResult: """ Create a new file with the specified content. Args: path: The path to the file to create content: The content for the new file Returns: FileOperationResult with result or error message """ log_info("file_editor", f"Creating file {path}") # Use the file writer to create the file return FileWriter.create(path, content) @staticmethod def insert_line(path: str, line_num: int, content: str) -> FileOperationResult: """ Insert content at a specific line in a file. Args: path: The path to the file to modify line_num: The line number where to insert (1-indexed) content: The content to insert Returns: FileOperationResult with result or error message """ log_info("file_editor", f"Inserting at line {line_num} in file {path}") # First, read the file to check if it exists read_result = FileEditor.read(path) if not read_result.success: log_error("file_editor", f"Cannot modify file that can't be read: {read_result.message}") return read_result # Then, use the file writer to insert the line return FileWriter.insert(path, line_num, content) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent/file_writer.py ================================================ #!/usr/bin/env python3 """ File writer for the Vertical Slice Architecture implementation of the file editor agent. This module provides file writing capabilities by composing various tools. """ import sys import os from typing import Dict, Any # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.file_operations.model_tools import FileOperationResult from features.file_operations.write_tool import write_file from features.file_operations.replace_tool import replace_in_file from features.file_operations.insert_tool import insert_in_file from features.file_operations.create_tool import create_file class FileWriter: """ File writer that composes various tools to provide file writing capabilities. """ @staticmethod def write(path: str, content: str) -> FileOperationResult: """ Write content to a file. Args: path: The path to the file to write content: The content to write to the file Returns: FileOperationResult with result or error message """ log_info("file_writer", f"Writing to file {path}") return write_file(path, content) @staticmethod def replace(path: str, old_str: str, new_str: str) -> FileOperationResult: """ Replace a string in a file. Args: path: The path to the file to modify old_str: The string to replace new_str: The string to replace with Returns: FileOperationResult with result or error message """ log_info("file_writer", f"Replacing text in file {path}") return replace_in_file(path, old_str, new_str) @staticmethod def insert(path: str, insert_line: int, new_str: str) -> FileOperationResult: """ Insert text at a specific line in a file. Args: path: The path to the file to modify insert_line: The line number after which to insert the text (1-indexed) new_str: The text to insert Returns: FileOperationResult with result or error message """ log_info("file_writer", f"Inserting text at line {insert_line} in file {path}") return insert_in_file(path, insert_line, new_str) @staticmethod def create(path: str, content: str) -> FileOperationResult: """ Create a new file with the specified content. Args: path: The path to the file to create content: The content to write to the file Returns: FileOperationResult with result or error message """ log_info("file_writer", f"Creating file {path}") return create_file(path, content) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent/insert_tool.py ================================================ #!/usr/bin/env python3 """ Insert tool for the Vertical Slice Architecture implementation of the file editor agent. This module provides line insertion capabilities for files. """ import sys import os # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.file_operations.model_tools import FileOperationResult def insert_in_file(path: str, insert_line: int, new_str: str) -> FileOperationResult: """ Insert text at a specific line in a file. Args: path: The path to the file to modify insert_line: The line number after which to insert the text (1-indexed) new_str: The text to insert Returns: FileOperationResult with result or error message """ log_info("insert_tool", f"Inserting text at line {insert_line} in file {path}") try: # Read the existing content with open(path, 'r', encoding='utf-8') as f: lines = f.readlines() if insert_line < 1 or insert_line > len(lines) + 1: error_msg = f"Invalid line number {insert_line} for file {path} with {len(lines)} lines" log_error("insert_tool", error_msg) return FileOperationResult(success=False, content="", message=error_msg) # Insert the new string at the specified position lines.insert(insert_line - 1, new_str if new_str.endswith('\n') else new_str + '\n') # Write the modified content back to the file with open(path, 'w', encoding='utf-8') as f: f.writelines(lines) log_info("insert_tool", f"Successfully inserted text at line {insert_line} in file {path}") return FileOperationResult(success=True, content="", message=f"Successfully inserted text at line {insert_line} in file {path}") except Exception as e: error_msg = f"Failed to insert text at line {insert_line} in file {path}: {str(e)}" log_error("insert_tool", error_msg) return FileOperationResult(success=False, content="", message=error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent/model_tools.py ================================================ #!/usr/bin/env python3 """ Models for the file operations feature in the Vertical Slice Architecture. """ import os import sys from typing import Dict, Any, Optional, List, Union # Add the project root to the Python path sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../'))) class FileOperationResult: """ Model representing the result of a file operation. """ def __init__(self, success: bool, message: str, content: str = "", data: Any = None): """ Initialize a file operation result. Args: success: Whether the operation was successful message: A message describing the result content: File content if the operation returns content data: Optional data returned by the operation """ self.success = success self.message = message self.content = content self.data = data def to_dict(self) -> Dict[str, Any]: """ Convert the result to a dictionary. Returns: Dictionary representation of the result """ return { "success": self.success, "message": self.message, "content": self.content, "data": self.data } class ToolUseRequest: """ Model representing a tool use request from Claude. """ def __init__(self, command: str, path: str = None, **kwargs): """ Initialize a tool use request. Args: command: The command to execute path: The path to operate on **kwargs: Additional arguments for the command """ self.command = command self.path = path self.kwargs = kwargs @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'ToolUseRequest': """ Create a tool use request from a dictionary. Args: data: Dictionary containing the tool use request Returns: A ToolUseRequest instance """ command = data.get("command") path = data.get("path") # Extract all other keys as kwargs kwargs = {k: v for k, v in data.items() if k not in ["command", "path"]} return cls(command, path, **kwargs) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent/read_tool.py ================================================ #!/usr/bin/env python3 """ Read tool for the Vertical Slice Architecture implementation of the file editor agent. This module provides file reading capabilities. """ import sys import os from typing import Optional # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.file_operations.model_tools import FileOperationResult def read_file(path: str, start_line: Optional[int] = None, end_line: Optional[int] = None) -> FileOperationResult: """ Read the contents of a file. Args: path: The path to the file to read start_line: Optional start line (1-indexed) end_line: Optional end line (1-indexed, -1 for end of file) Returns: FileOperationResult with content or error message """ log_info("read_tool", f"Reading file {path} with range {start_line}-{end_line}") try: with open(path, 'r', encoding='utf-8') as f: all_lines = f.readlines() # Handle line range if start_line is not None: start_idx = max(0, start_line - 1) # Convert 1-indexed to 0-indexed else: start_idx = 0 if end_line is not None: if end_line == -1: end_idx = len(all_lines) else: end_idx = min(end_line, len(all_lines)) else: end_idx = len(all_lines) selected_lines = all_lines[start_idx:end_idx] content = ''.join(selected_lines) log_info("read_tool", f"Successfully read file {path}") return FileOperationResult(success=True, content=content, message=f"Successfully read file {path}") except Exception as e: error_msg = f"Failed to read file {path}: {str(e)}" log_error("read_tool", error_msg) return FileOperationResult(success=False, content="", message=error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent/replace_tool.py ================================================ #!/usr/bin/env python3 """ Replace tool for the Vertical Slice Architecture implementation of the file editor agent. This module provides string replacement capabilities for files. """ import sys import os # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.file_operations.model_tools import FileOperationResult def replace_in_file(path: str, old_str: str, new_str: str) -> FileOperationResult: """ Replace a string in a file. Args: path: The path to the file to modify old_str: The string to replace new_str: The string to replace with Returns: FileOperationResult with result or error message """ log_info("replace_tool", f"Replacing text in file {path}") try: # Read the existing content with open(path, 'r', encoding='utf-8') as f: content = f.read() # Count occurrences to verify uniqueness occurrences = content.count(old_str) if occurrences == 0: error_msg = f"String not found in file {path}" log_error("replace_tool", error_msg) return FileOperationResult(success=False, content="", message=error_msg) if occurrences > 1: error_msg = f"Multiple occurrences ({occurrences}) of the string found in file {path}. Need a unique string to replace." log_error("replace_tool", error_msg) return FileOperationResult(success=False, content="", message=error_msg) # Replace the string and write back to the file new_content = content.replace(old_str, new_str, 1) with open(path, 'w', encoding='utf-8') as f: f.write(new_content) log_info("replace_tool", f"Successfully replaced text in file {path}") return FileOperationResult(success=True, content="", message=f"Successfully replaced text in file {path}") except Exception as e: error_msg = f"Failed to replace text in file {path}: {str(e)}" log_error("replace_tool", error_msg) return FileOperationResult(success=False, content="", message=error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent/service_tools.py ================================================ #!/usr/bin/env python3 """ Service layer for file operations in the Vertical Slice Architecture. """ import os import sys import traceback from typing import Dict, Any, Optional, List, Tuple, Union # Add the project root to the Python path sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../'))) from shared.utils import console, normalize_path, display_file_content from features.file_operations.model import FileOperationResult class FileOperationService: """ Service for handling file operations. """ @staticmethod def view_file(path: str, view_range=None) -> FileOperationResult: """ View the contents of a file. Args: path: The path to the file to view view_range: Optional start and end lines to view [start, end] Returns: FileOperationResult with content or error message """ try: # Normalize the path path = normalize_path(path) if not os.path.exists(path): error_msg = f"File {path} does not exist" console.log(f"[view_file] Error: {error_msg}") return FileOperationResult(False, error_msg) with open(path, "r") as f: lines = f.readlines() if view_range: start, end = view_range # Convert to 0-indexed for Python start = max(0, start - 1) if end == -1: end = len(lines) else: end = min(len(lines), end) lines = lines[start:end] content = "".join(lines) # Display the file content (only for console, not returned to Claude) display_file_content(path, content) return FileOperationResult(True, f"Successfully viewed file {path}", content) except Exception as e: error_msg = f"Error viewing file: {str(e)}" console.print(f"[red]{error_msg}[/red]") console.log(f"[view_file] Error: {str(e)}") console.log(traceback.format_exc()) return FileOperationResult(False, error_msg) @staticmethod def str_replace(path: str, old_str: str, new_str: str) -> FileOperationResult: """ Replace a specific string in a file. Args: path: The path to the file to modify old_str: The text to replace new_str: The new text to insert Returns: FileOperationResult with result or error message """ try: # Normalize the path path = normalize_path(path) if not os.path.exists(path): error_msg = f"File {path} does not exist" console.log(f"[str_replace] Error: {error_msg}") return FileOperationResult(False, error_msg) with open(path, "r") as f: content = f.read() if old_str not in content: error_msg = f"The specified string was not found in the file {path}" console.log(f"[str_replace] Error: {error_msg}") return FileOperationResult(False, error_msg) new_content = content.replace(old_str, new_str, 1) with open(path, "w") as f: f.write(new_content) console.print(f"[green]Successfully replaced text in {path}[/green]") console.log(f"[str_replace] Successfully replaced text in {path}") return FileOperationResult(True, f"Successfully replaced text in {path}") except Exception as e: error_msg = f"Error replacing text: {str(e)}" console.print(f"[red]{error_msg}[/red]") console.log(f"[str_replace] Error: {str(e)}") console.log(traceback.format_exc()) return FileOperationResult(False, error_msg) @staticmethod def create_file(path: str, file_text: str) -> FileOperationResult: """ Create a new file with specified content. Args: path: The path where the new file should be created file_text: The content to write to the new file Returns: FileOperationResult with result or error message """ try: # Check if the path is empty or invalid if not path or not path.strip(): error_msg = "Invalid file path provided: path is empty." console.log(f"[create_file] Error: {error_msg}") return FileOperationResult(False, error_msg) # Normalize the path path = normalize_path(path) # Check if the directory exists directory = os.path.dirname(path) if directory and not os.path.exists(directory): console.log(f"[create_file] Creating directory: {directory}") os.makedirs(directory) with open(path, "w") as f: f.write(file_text or "") console.print(f"[green]Successfully created file {path}[/green]") console.log(f"[create_file] Successfully created file {path}") return FileOperationResult(True, f"Successfully created file {path}") except Exception as e: error_msg = f"Error creating file: {str(e)}" console.print(f"[red]{error_msg}[/red]") console.log(f"[create_file] Error: {str(e)}") console.log(traceback.format_exc()) return FileOperationResult(False, error_msg) @staticmethod def insert_text(path: str, insert_line: int, new_str: str) -> FileOperationResult: """ Insert text at a specific location in a file. Args: path: The path to the file to modify insert_line: The line number after which to insert the text new_str: The text to insert Returns: FileOperationResult with result or error message """ try: if not path or not path.strip(): error_msg = "Invalid file path provided: path is empty." console.log(f"[insert_text] Error: {error_msg}") return FileOperationResult(False, error_msg) # Normalize the path path = normalize_path(path) if not os.path.exists(path): error_msg = f"File {path} does not exist" console.log(f"[insert_text] Error: {error_msg}") return FileOperationResult(False, error_msg) if insert_line is None: error_msg = "No line number specified: insert_line is missing." console.log(f"[insert_text] Error: {error_msg}") return FileOperationResult(False, error_msg) with open(path, "r") as f: lines = f.readlines() # Line is 0-indexed for this function, but Claude provides 1-indexed insert_line = min(max(0, insert_line - 1), len(lines)) # Check that the index is within acceptable bounds if insert_line < 0 or insert_line > len(lines): error_msg = ( f"Insert line number {insert_line} out of range (0-{len(lines)})." ) console.log(f"[insert_text] Error: {error_msg}") return FileOperationResult(False, error_msg) # Ensure new_str ends with newline if new_str and not new_str.endswith("\n"): new_str += "\n" lines.insert(insert_line, new_str) with open(path, "w") as f: f.writelines(lines) console.print( f"[green]Successfully inserted text at line {insert_line + 1} in {path}[/green]" ) console.log( f"[insert_text] Successfully inserted text at line {insert_line + 1} in {path}" ) return FileOperationResult( True, f"Successfully inserted text at line {insert_line + 1} in {path}" ) except Exception as e: error_msg = f"Error inserting text: {str(e)}" console.print(f"[red]{error_msg}[/red]") console.log(f"[insert_text] Error: {str(e)}") console.log(traceback.format_exc()) return FileOperationResult(False, error_msg) @staticmethod def undo_edit(path: str) -> FileOperationResult: """ Placeholder for undo_edit functionality. In a real implementation, you would need to track edit history. Args: path: The path to the file whose last edit should be undone Returns: FileOperationResult with message about undo functionality """ try: if not path or not path.strip(): error_msg = "Invalid file path provided: path is empty." console.log(f"[undo_edit] Error: {error_msg}") return FileOperationResult(False, error_msg) # Normalize the path path = normalize_path(path) message = "Undo functionality is not implemented in this version." console.print(f"[yellow]{message}[/yellow]") console.log(f"[undo_edit] {message}") return FileOperationResult(True, message) except Exception as e: error_msg = f"Error in undo_edit: {str(e)}" console.print(f"[red]{error_msg}[/red]") console.log(f"[undo_edit] Error: {str(e)}") console.log(traceback.format_exc()) return FileOperationResult(False, error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent/tool_handler.py ================================================ #!/usr/bin/env python3 """ Tool handler for the Vertical Slice Architecture implementation of the file editor agent. This module handles tool use requests from the Claude agent. """ import sys import os from typing import Dict, Any # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error, normalize_path from features.file_operations.model_tools import ToolUseRequest from features.file_operations.file_editor import FileEditor def handle_tool_use(input_data: Dict[str, Any]) -> Dict[str, Any]: """ Handle tool use requests from the Claude agent. Args: input_data: The tool use request data from Claude Returns: Dictionary with the result or error message """ log_info("tool_handler", f"Received tool use request: {input_data}") try: # Parse the tool use request request = ToolUseRequest.from_dict(input_data) # Normalize the path path = normalize_path(request.path) if request.path else None # Handle the command if request.command == "view": start_line = request.kwargs.get("start_line") end_line = request.kwargs.get("end_line") if start_line is not None: start_line = int(start_line) if end_line is not None: end_line = int(end_line) result = FileEditor.read(path, start_line, end_line) elif request.command == "edit": old_str = request.kwargs.get("old_str", "") new_str = request.kwargs.get("new_str", "") result = FileEditor.edit_file(path, old_str, new_str) elif request.command == "create": content = request.kwargs.get("content", "") result = FileEditor.create_file(path, content) elif request.command == "insert": line_num = int(request.kwargs.get("line_num", 1)) content = request.kwargs.get("content", "") result = FileEditor.insert_line(path, line_num, content) else: log_error("tool_handler", f"Unknown command: {request.command}") return {"error": f"Unknown command: {request.command}"} # Return the result if result.success: return {"result": result.content or result.message} else: return {"error": result.message} except Exception as e: error_msg = f"Error handling tool use: {str(e)}" log_error("tool_handler", error_msg) return {"error": error_msg} ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent/write_tool.py ================================================ #!/usr/bin/env python3 """ Write tool for the Vertical Slice Architecture implementation of the file editor agent. This module provides file writing capabilities. """ import sys import os # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.file_operations.model_tools import FileOperationResult def write_file(path: str, content: str) -> FileOperationResult: """ Write content to a file. Args: path: The path to the file to write content: The content to write to the file Returns: FileOperationResult with result or error message """ log_info("write_tool", f"Writing to file {path}") try: with open(path, 'w', encoding='utf-8') as f: f.write(content) log_info("write_tool", f"Successfully wrote to file {path}") return FileOperationResult(success=True, content="", message=f"Successfully wrote to file {path}") except Exception as e: error_msg = f"Failed to write to file {path}: {str(e)}" log_error("write_tool", error_msg) return FileOperationResult(success=False, content="", message=error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2/__init__.py ================================================ ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2/api_tools.py ================================================ #!/usr/bin/env python3 """ API layer for file operations in the Vertical Slice Architecture. """ import os import sys import traceback from typing import Dict, Any, Optional, List, Union # Add the project root to the Python path sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../'))) from shared.utils import console from features.file_operations.service import FileOperationService from features.file_operations.model import ToolUseRequest, FileOperationResult class FileOperationsAPI: """ API for file operations. """ @staticmethod def handle_tool_use(tool_use: Dict[str, Any]) -> Dict[str, Any]: """ Handle text editor tool use from Claude. Args: tool_use: The tool use request from Claude Returns: Dictionary with result or error to send back to Claude """ try: # Convert the tool use dictionary to a ToolUseRequest object request = ToolUseRequest.from_dict(tool_use) console.log(f"[handle_tool_use] Received command: {request.command}, path: {request.path}") if not request.command: error_msg = "No command specified in tool use request" console.log(f"[handle_tool_use] Error: {error_msg}") return {"error": error_msg} if not request.path and request.command != "undo_edit": # undo_edit might not need a path error_msg = "No path specified in tool use request" console.log(f"[handle_tool_use] Error: {error_msg}") return {"error": error_msg} # The path normalization is now handled in each file operation function console.print(f"[blue]Executing {request.command} command on {request.path}[/blue]") result = None if request.command == "view": view_range = request.kwargs.get("view_range") console.log( f"[handle_tool_use] Calling view_file with view_range: {view_range}" ) result = FileOperationService.view_file(request.path, view_range) elif request.command == "str_replace": old_str = request.kwargs.get("old_str") new_str = request.kwargs.get("new_str") console.log(f"[handle_tool_use] Calling str_replace") result = FileOperationService.str_replace(request.path, old_str, new_str) elif request.command == "create": file_text = request.kwargs.get("file_text") console.log(f"[handle_tool_use] Calling create_file") result = FileOperationService.create_file(request.path, file_text) elif request.command == "insert": insert_line = request.kwargs.get("insert_line") new_str = request.kwargs.get("new_str") console.log(f"[handle_tool_use] Calling insert_text at line: {insert_line}") result = FileOperationService.insert_text(request.path, insert_line, new_str) elif request.command == "undo_edit": console.log(f"[handle_tool_use] Calling undo_edit") result = FileOperationService.undo_edit(request.path) else: error_msg = f"Unknown command: {request.command}" console.print(f"[red]{error_msg}[/red]") console.log(f"[handle_tool_use] Error: {error_msg}") return {"error": error_msg} # Convert the result to a dictionary if result.success: return {"result": result.data if result.data is not None else result.message} else: return {"error": result.message} except Exception as e: error_msg = f"Error handling tool use: {str(e)}" console.print(f"[red]{error_msg}[/red]") console.log(f"[handle_tool_use] Error: {str(e)}") console.log(traceback.format_exc()) return {"error": error_msg} ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2/create_tool.py ================================================ #!/usr/bin/env python3 """ Create tool for the Vertical Slice Architecture implementation of the file editor agent. This module provides file creation capabilities. """ import sys import os # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.file_operations.model_tools import FileOperationResult from features.file_operations.write_tool import write_file def create_file(path: str, content: str) -> FileOperationResult: """ Create a new file with the specified content. Args: path: The path to the file to create content: The content to write to the file Returns: FileOperationResult with result or error message """ log_info("create_tool", f"Creating file {path}") try: # Create directory if it doesn't exist os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True) # Use the write_file function to create the file return write_file(path, content) except Exception as e: error_msg = f"Failed to create file {path}: {str(e)}" log_error("create_tool", error_msg) return FileOperationResult(success=False, content="", message=error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2/file_agent.py ================================================ #!/usr/bin/env python3 """ File agent for the Vertical Slice Architecture implementation of the file editor agent. This module provides the agent interface for file operations. """ import time from typing import Tuple, Dict, Any, List, Optional, Callable from rich.console import Console from rich.panel import Panel from anthropic import Anthropic import sys import os # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error, display_token_usage from features.file_operations.model_tools import FileOperationResult from features.file_operations.tool_handler import handle_tool_use # Initialize rich console console = Console() # Define constants MODEL = "claude-3-7-sonnet-20250219" DEFAULT_THINKING_TOKENS = 3000 class FileAgent: """ File agent that provides an interface for AI-assisted file operations. """ @staticmethod def run_agent( client: Anthropic, prompt: str, max_thinking_tokens: int = DEFAULT_THINKING_TOKENS, max_loops: int = 10, use_token_efficiency: bool = False, ) -> Tuple[str, int, int]: """ Run the Claude agent with file editing capabilities. Args: client: The Anthropic client prompt: The user's prompt max_thinking_tokens: Maximum tokens for thinking max_loops: Maximum number of tool use loops use_token_efficiency: Whether to use token-efficient tool use beta feature Returns: Tuple containing: - Final response from Claude (str) - Total input tokens used (int) - Total output tokens used (int) """ # Track token usage input_tokens_total = 0 output_tokens_total = 0 system_prompt = """You are a helpful AI assistant with text editing capabilities. You have access to a text editor tool that can view, edit, and create files. Always think step by step about what you need to do before taking any action. Be careful when making edits to files, as they can permanently change the user's files. Follow these steps when handling file operations: 1. First, view files to understand their content before making changes 2. For edits, ensure you have the correct context and are making the right changes 3. When creating files, make sure they're in the right location with proper formatting """ # Define text editor tool text_editor_tool = {"name": "str_replace_editor", "type": "text_editor_20250124"} messages = [ { "role": "user", "content": f"""I need help with editing files. Here's what I want to do: {prompt} Please use the text editor tool to help me with this. First, think through what you need to do, then use the appropriate tool. """, } ] loop_count = 0 tool_use_count = 0 thinking_start_time = time.time() while loop_count < max_loops: loop_count += 1 console.rule(f"[yellow]Agent Loop {loop_count}/{max_loops}[/yellow]") log_info("file_agent", f"Starting agent loop {loop_count}/{max_loops}") # Create message with text editor tool message_args = { "model": MODEL, "max_tokens": 4096, "tools": [text_editor_tool], "messages": messages, "system": system_prompt, "thinking": {"type": "enabled", "budget_tokens": max_thinking_tokens}, } # Use the beta.messages with betas parameter if token efficiency is enabled if use_token_efficiency: # Using token-efficient tools beta feature message_args["betas"] = ["token-efficient-tools-2025-02-19"] response = client.beta.messages.create(**message_args) else: # Standard approach response = client.messages.create(**message_args) # Track token usage if hasattr(response, "usage"): input_tokens = getattr(response.usage, "input_tokens", 0) output_tokens = getattr(response.usage, "output_tokens", 0) input_tokens_total += input_tokens output_tokens_total += output_tokens console.print( f"[dim]Loop {loop_count} tokens: Input={input_tokens}, Output={output_tokens}[/dim]" ) log_info( "file_agent", f"Loop {loop_count} tokens: Input={input_tokens}, Output={output_tokens}" ) # Process response content thinking_block = None tool_use_block = None text_block = None for content_block in response.content: if content_block.type == "thinking": thinking_block = content_block # Access the thinking attribute which contains the actual thinking text if hasattr(thinking_block, "thinking"): console.print( Panel( thinking_block.thinking, title=f"Claude's Thinking (Loop {loop_count})", border_style="blue", ) ) else: console.print( Panel( "Claude is thinking...", title=f"Claude's Thinking (Loop {loop_count})", border_style="blue", ) ) elif content_block.type == "tool_use": tool_use_block = content_block tool_use_count += 1 elif content_block.type == "text": text_block = content_block # If we got a final text response with no tool use, we're done if text_block and not tool_use_block: thinking_end_time = time.time() thinking_duration = thinking_end_time - thinking_start_time console.print( f"\n[bold green]Completed in {thinking_duration:.2f} seconds after {loop_count} loops and {tool_use_count} tool uses[/bold green]" ) log_info( "file_agent", f"Completed in {thinking_duration:.2f} seconds after {loop_count} loops and {tool_use_count} tool uses" ) # Add the response to messages messages.append( { "role": "assistant", "content": [ *([thinking_block] if thinking_block else []), {"type": "text", "text": text_block.text}, ], } ) return text_block.text, input_tokens_total, output_tokens_total # Handle tool use if tool_use_block: # Add the assistant's response to messages before handling tool calls messages.append({"role": "assistant", "content": response.content}) console.print( f"\n[bold blue]Tool Call:[/bold blue] {tool_use_block.name}" ) log_info("file_agent", f"Tool Call: {tool_use_block.name}") # Handle the tool use with our handler tool_result = handle_tool_use(tool_use_block.input) # Format tool result for Claude tool_result_message = { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_use_block.id, "content": tool_result.get("error") or tool_result.get("result", ""), } ], } messages.append(tool_result_message) # If we reach here, we hit the max loops console.print( f"\n[bold red]Warning: Reached maximum loops ({max_loops}) without completing the task[/bold red]" ) log_error( "file_agent", f"Reached maximum loops ({max_loops}) without completing the task" ) return ( "I wasn't able to complete the task within the allowed number of thinking steps. Please try a more specific prompt or increase the loop limit.", input_tokens_total, output_tokens_total, ) # Expose the run_agent function at the module level def run_agent( client: Anthropic, prompt: str, max_tool_use_loops: int = 15, token_efficient_tool_use: bool = True, ) -> Tuple[int, int]: """ Run the file editor agent with the specified prompt. Args: client: The Anthropic client prompt: The prompt to send to Claude max_tool_use_loops: Maximum number of tool use loops token_efficient_tool_use: Whether to use token-efficient tool use Returns: Tuple containing input and output token counts """ log_info("file_agent", f"Running agent with prompt: {prompt}") _, input_tokens, output_tokens = FileAgent.run_agent( client=client, prompt=prompt, max_loops=max_tool_use_loops, use_token_efficiency=token_efficient_tool_use, max_thinking_tokens=DEFAULT_THINKING_TOKENS ) return input_tokens, output_tokens ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2/file_editor.py ================================================ #!/usr/bin/env python3 """ File editor for the Vertical Slice Architecture implementation of the file editor agent. This module combines reading and writing capabilities for file editing. """ import sys import os from typing import Dict, Any, Tuple, Optional # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.file_operations.model_tools import FileOperationResult from features.file_operations.file_writer import FileWriter from features.file_operations.read_tool import read_file class FileEditor: """ File editor that combines reading and writing capabilities for file editing. """ @staticmethod def read(path: str, start_line: Optional[int] = None, end_line: Optional[int] = None) -> FileOperationResult: """ Read the contents of a file. Args: path: The path to the file to read start_line: Optional start line (1-indexed) end_line: Optional end line (1-indexed, -1 for end of file) Returns: FileOperationResult with content or error message """ log_info("file_editor", f"Reading file {path} with range {start_line}-{end_line}") return read_file(path, start_line, end_line) @staticmethod def view_file(path: str, view_range=None) -> FileOperationResult: """ View the contents of a file with optional range. Args: path: The path to the file to view view_range: Optional tuple of (start_line, end_line) Returns: FileOperationResult with content or error message """ start_line = None end_line = None if view_range: start_line, end_line = view_range log_info("file_editor", f"Viewing file {path} with range {start_line}-{end_line}") return FileEditor.read(path, start_line, end_line) @staticmethod def edit_file(path: str, old_str: str, new_str: str) -> FileOperationResult: """ Edit a file by replacing one string with another. Args: path: The path to the file to edit old_str: The string to replace new_str: The string to replace it with Returns: FileOperationResult with result or error message """ log_info("file_editor", f"Editing file {path}") # First, read the file to check if it exists read_result = FileEditor.read(path) if not read_result.success: log_error("file_editor", f"Cannot edit file that can't be read: {read_result.message}") return read_result # Then, use the file writer to replace the string return FileWriter.replace(path, old_str, new_str) @staticmethod def create_file(path: str, content: str) -> FileOperationResult: """ Create a new file with the specified content. Args: path: The path to the file to create content: The content for the new file Returns: FileOperationResult with result or error message """ log_info("file_editor", f"Creating file {path}") # Use the file writer to create the file return FileWriter.create(path, content) @staticmethod def insert_line(path: str, line_num: int, content: str) -> FileOperationResult: """ Insert content at a specific line in a file. Args: path: The path to the file to modify line_num: The line number where to insert (1-indexed) content: The content to insert Returns: FileOperationResult with result or error message """ log_info("file_editor", f"Inserting at line {line_num} in file {path}") # First, read the file to check if it exists read_result = FileEditor.read(path) if not read_result.success: log_error("file_editor", f"Cannot modify file that can't be read: {read_result.message}") return read_result # Then, use the file writer to insert the line return FileWriter.insert(path, line_num, content) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2/file_writer.py ================================================ #!/usr/bin/env python3 """ File writer for the Vertical Slice Architecture implementation of the file editor agent. This module provides file writing capabilities by composing various tools. """ import sys import os from typing import Dict, Any # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.file_operations.model_tools import FileOperationResult from features.file_operations.write_tool import write_file from features.file_operations.replace_tool import replace_in_file from features.file_operations.insert_tool import insert_in_file from features.file_operations.create_tool import create_file class FileWriter: """ File writer that composes various tools to provide file writing capabilities. """ @staticmethod def write(path: str, content: str) -> FileOperationResult: """ Write content to a file. Args: path: The path to the file to write content: The content to write to the file Returns: FileOperationResult with result or error message """ log_info("file_writer", f"Writing to file {path}") return write_file(path, content) @staticmethod def replace(path: str, old_str: str, new_str: str) -> FileOperationResult: """ Replace a string in a file. Args: path: The path to the file to modify old_str: The string to replace new_str: The string to replace with Returns: FileOperationResult with result or error message """ log_info("file_writer", f"Replacing text in file {path}") return replace_in_file(path, old_str, new_str) @staticmethod def insert(path: str, insert_line: int, new_str: str) -> FileOperationResult: """ Insert text at a specific line in a file. Args: path: The path to the file to modify insert_line: The line number after which to insert the text (1-indexed) new_str: The text to insert Returns: FileOperationResult with result or error message """ log_info("file_writer", f"Inserting text at line {insert_line} in file {path}") return insert_in_file(path, insert_line, new_str) @staticmethod def create(path: str, content: str) -> FileOperationResult: """ Create a new file with the specified content. Args: path: The path to the file to create content: The content to write to the file Returns: FileOperationResult with result or error message """ log_info("file_writer", f"Creating file {path}") return create_file(path, content) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2/insert_tool.py ================================================ #!/usr/bin/env python3 """ Insert tool for the Vertical Slice Architecture implementation of the file editor agent. This module provides line insertion capabilities for files. """ import sys import os # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.file_operations.model_tools import FileOperationResult def insert_in_file(path: str, insert_line: int, new_str: str) -> FileOperationResult: """ Insert text at a specific line in a file. Args: path: The path to the file to modify insert_line: The line number after which to insert the text (1-indexed) new_str: The text to insert Returns: FileOperationResult with result or error message """ log_info("insert_tool", f"Inserting text at line {insert_line} in file {path}") try: # Read the existing content with open(path, 'r', encoding='utf-8') as f: lines = f.readlines() if insert_line < 1 or insert_line > len(lines) + 1: error_msg = f"Invalid line number {insert_line} for file {path} with {len(lines)} lines" log_error("insert_tool", error_msg) return FileOperationResult(success=False, content="", message=error_msg) # Insert the new string at the specified position lines.insert(insert_line - 1, new_str if new_str.endswith('\n') else new_str + '\n') # Write the modified content back to the file with open(path, 'w', encoding='utf-8') as f: f.writelines(lines) log_info("insert_tool", f"Successfully inserted text at line {insert_line} in file {path}") return FileOperationResult(success=True, content="", message=f"Successfully inserted text at line {insert_line} in file {path}") except Exception as e: error_msg = f"Failed to insert text at line {insert_line} in file {path}: {str(e)}" log_error("insert_tool", error_msg) return FileOperationResult(success=False, content="", message=error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2/model_tools.py ================================================ #!/usr/bin/env python3 """ Models for the file operations feature in the Vertical Slice Architecture. """ import os import sys from typing import Dict, Any, Optional, List, Union # Add the project root to the Python path sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../'))) class FileOperationResult: """ Model representing the result of a file operation. """ def __init__(self, success: bool, message: str, content: str = "", data: Any = None): """ Initialize a file operation result. Args: success: Whether the operation was successful message: A message describing the result content: File content if the operation returns content data: Optional data returned by the operation """ self.success = success self.message = message self.content = content self.data = data def to_dict(self) -> Dict[str, Any]: """ Convert the result to a dictionary. Returns: Dictionary representation of the result """ return { "success": self.success, "message": self.message, "content": self.content, "data": self.data } class ToolUseRequest: """ Model representing a tool use request from Claude. """ def __init__(self, command: str, path: str = None, **kwargs): """ Initialize a tool use request. Args: command: The command to execute path: The path to operate on **kwargs: Additional arguments for the command """ self.command = command self.path = path self.kwargs = kwargs @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'ToolUseRequest': """ Create a tool use request from a dictionary. Args: data: Dictionary containing the tool use request Returns: A ToolUseRequest instance """ command = data.get("command") path = data.get("path") # Extract all other keys as kwargs kwargs = {k: v for k, v in data.items() if k not in ["command", "path"]} return cls(command, path, **kwargs) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2/read_tool.py ================================================ #!/usr/bin/env python3 """ Read tool for the Vertical Slice Architecture implementation of the file editor agent. This module provides file reading capabilities. """ import sys import os from typing import Optional # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.file_operations.model_tools import FileOperationResult def read_file(path: str, start_line: Optional[int] = None, end_line: Optional[int] = None) -> FileOperationResult: """ Read the contents of a file. Args: path: The path to the file to read start_line: Optional start line (1-indexed) end_line: Optional end line (1-indexed, -1 for end of file) Returns: FileOperationResult with content or error message """ log_info("read_tool", f"Reading file {path} with range {start_line}-{end_line}") try: with open(path, 'r', encoding='utf-8') as f: all_lines = f.readlines() # Handle line range if start_line is not None: start_idx = max(0, start_line - 1) # Convert 1-indexed to 0-indexed else: start_idx = 0 if end_line is not None: if end_line == -1: end_idx = len(all_lines) else: end_idx = min(end_line, len(all_lines)) else: end_idx = len(all_lines) selected_lines = all_lines[start_idx:end_idx] content = ''.join(selected_lines) log_info("read_tool", f"Successfully read file {path}") return FileOperationResult(success=True, content=content, message=f"Successfully read file {path}") except Exception as e: error_msg = f"Failed to read file {path}: {str(e)}" log_error("read_tool", error_msg) return FileOperationResult(success=False, content="", message=error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2/replace_tool.py ================================================ #!/usr/bin/env python3 """ Replace tool for the Vertical Slice Architecture implementation of the file editor agent. This module provides string replacement capabilities for files. """ import sys import os # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.file_operations.model_tools import FileOperationResult def replace_in_file(path: str, old_str: str, new_str: str) -> FileOperationResult: """ Replace a string in a file. Args: path: The path to the file to modify old_str: The string to replace new_str: The string to replace with Returns: FileOperationResult with result or error message """ log_info("replace_tool", f"Replacing text in file {path}") try: # Read the existing content with open(path, 'r', encoding='utf-8') as f: content = f.read() # Count occurrences to verify uniqueness occurrences = content.count(old_str) if occurrences == 0: error_msg = f"String not found in file {path}" log_error("replace_tool", error_msg) return FileOperationResult(success=False, content="", message=error_msg) if occurrences > 1: error_msg = f"Multiple occurrences ({occurrences}) of the string found in file {path}. Need a unique string to replace." log_error("replace_tool", error_msg) return FileOperationResult(success=False, content="", message=error_msg) # Replace the string and write back to the file new_content = content.replace(old_str, new_str, 1) with open(path, 'w', encoding='utf-8') as f: f.write(new_content) log_info("replace_tool", f"Successfully replaced text in file {path}") return FileOperationResult(success=True, content="", message=f"Successfully replaced text in file {path}") except Exception as e: error_msg = f"Failed to replace text in file {path}: {str(e)}" log_error("replace_tool", error_msg) return FileOperationResult(success=False, content="", message=error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2/service_tools.py ================================================ #!/usr/bin/env python3 """ Service layer for file operations in the Vertical Slice Architecture. """ import os import sys import traceback from typing import Dict, Any, Optional, List, Tuple, Union # Add the project root to the Python path sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../'))) from shared.utils import console, normalize_path, display_file_content from features.file_operations.model import FileOperationResult class FileOperationService: """ Service for handling file operations. """ @staticmethod def view_file(path: str, view_range=None) -> FileOperationResult: """ View the contents of a file. Args: path: The path to the file to view view_range: Optional start and end lines to view [start, end] Returns: FileOperationResult with content or error message """ try: # Normalize the path path = normalize_path(path) if not os.path.exists(path): error_msg = f"File {path} does not exist" console.log(f"[view_file] Error: {error_msg}") return FileOperationResult(False, error_msg) with open(path, "r") as f: lines = f.readlines() if view_range: start, end = view_range # Convert to 0-indexed for Python start = max(0, start - 1) if end == -1: end = len(lines) else: end = min(len(lines), end) lines = lines[start:end] content = "".join(lines) # Display the file content (only for console, not returned to Claude) display_file_content(path, content) return FileOperationResult(True, f"Successfully viewed file {path}", content) except Exception as e: error_msg = f"Error viewing file: {str(e)}" console.print(f"[red]{error_msg}[/red]") console.log(f"[view_file] Error: {str(e)}") console.log(traceback.format_exc()) return FileOperationResult(False, error_msg) @staticmethod def str_replace(path: str, old_str: str, new_str: str) -> FileOperationResult: """ Replace a specific string in a file. Args: path: The path to the file to modify old_str: The text to replace new_str: The new text to insert Returns: FileOperationResult with result or error message """ try: # Normalize the path path = normalize_path(path) if not os.path.exists(path): error_msg = f"File {path} does not exist" console.log(f"[str_replace] Error: {error_msg}") return FileOperationResult(False, error_msg) with open(path, "r") as f: content = f.read() if old_str not in content: error_msg = f"The specified string was not found in the file {path}" console.log(f"[str_replace] Error: {error_msg}") return FileOperationResult(False, error_msg) new_content = content.replace(old_str, new_str, 1) with open(path, "w") as f: f.write(new_content) console.print(f"[green]Successfully replaced text in {path}[/green]") console.log(f"[str_replace] Successfully replaced text in {path}") return FileOperationResult(True, f"Successfully replaced text in {path}") except Exception as e: error_msg = f"Error replacing text: {str(e)}" console.print(f"[red]{error_msg}[/red]") console.log(f"[str_replace] Error: {str(e)}") console.log(traceback.format_exc()) return FileOperationResult(False, error_msg) @staticmethod def create_file(path: str, file_text: str) -> FileOperationResult: """ Create a new file with specified content. Args: path: The path where the new file should be created file_text: The content to write to the new file Returns: FileOperationResult with result or error message """ try: # Check if the path is empty or invalid if not path or not path.strip(): error_msg = "Invalid file path provided: path is empty." console.log(f"[create_file] Error: {error_msg}") return FileOperationResult(False, error_msg) # Normalize the path path = normalize_path(path) # Check if the directory exists directory = os.path.dirname(path) if directory and not os.path.exists(directory): console.log(f"[create_file] Creating directory: {directory}") os.makedirs(directory) with open(path, "w") as f: f.write(file_text or "") console.print(f"[green]Successfully created file {path}[/green]") console.log(f"[create_file] Successfully created file {path}") return FileOperationResult(True, f"Successfully created file {path}") except Exception as e: error_msg = f"Error creating file: {str(e)}" console.print(f"[red]{error_msg}[/red]") console.log(f"[create_file] Error: {str(e)}") console.log(traceback.format_exc()) return FileOperationResult(False, error_msg) @staticmethod def insert_text(path: str, insert_line: int, new_str: str) -> FileOperationResult: """ Insert text at a specific location in a file. Args: path: The path to the file to modify insert_line: The line number after which to insert the text new_str: The text to insert Returns: FileOperationResult with result or error message """ try: if not path or not path.strip(): error_msg = "Invalid file path provided: path is empty." console.log(f"[insert_text] Error: {error_msg}") return FileOperationResult(False, error_msg) # Normalize the path path = normalize_path(path) if not os.path.exists(path): error_msg = f"File {path} does not exist" console.log(f"[insert_text] Error: {error_msg}") return FileOperationResult(False, error_msg) if insert_line is None: error_msg = "No line number specified: insert_line is missing." console.log(f"[insert_text] Error: {error_msg}") return FileOperationResult(False, error_msg) with open(path, "r") as f: lines = f.readlines() # Line is 0-indexed for this function, but Claude provides 1-indexed insert_line = min(max(0, insert_line - 1), len(lines)) # Check that the index is within acceptable bounds if insert_line < 0 or insert_line > len(lines): error_msg = ( f"Insert line number {insert_line} out of range (0-{len(lines)})." ) console.log(f"[insert_text] Error: {error_msg}") return FileOperationResult(False, error_msg) # Ensure new_str ends with newline if new_str and not new_str.endswith("\n"): new_str += "\n" lines.insert(insert_line, new_str) with open(path, "w") as f: f.writelines(lines) console.print( f"[green]Successfully inserted text at line {insert_line + 1} in {path}[/green]" ) console.log( f"[insert_text] Successfully inserted text at line {insert_line + 1} in {path}" ) return FileOperationResult( True, f"Successfully inserted text at line {insert_line + 1} in {path}" ) except Exception as e: error_msg = f"Error inserting text: {str(e)}" console.print(f"[red]{error_msg}[/red]") console.log(f"[insert_text] Error: {str(e)}") console.log(traceback.format_exc()) return FileOperationResult(False, error_msg) @staticmethod def undo_edit(path: str) -> FileOperationResult: """ Placeholder for undo_edit functionality. In a real implementation, you would need to track edit history. Args: path: The path to the file whose last edit should be undone Returns: FileOperationResult with message about undo functionality """ try: if not path or not path.strip(): error_msg = "Invalid file path provided: path is empty." console.log(f"[undo_edit] Error: {error_msg}") return FileOperationResult(False, error_msg) # Normalize the path path = normalize_path(path) message = "Undo functionality is not implemented in this version." console.print(f"[yellow]{message}[/yellow]") console.log(f"[undo_edit] {message}") return FileOperationResult(True, message) except Exception as e: error_msg = f"Error in undo_edit: {str(e)}" console.print(f"[red]{error_msg}[/red]") console.log(f"[undo_edit] Error: {str(e)}") console.log(traceback.format_exc()) return FileOperationResult(False, error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2/tool_handler.py ================================================ #!/usr/bin/env python3 """ Tool handler for the Vertical Slice Architecture implementation of the file editor agent. This module handles tool use requests from the Claude agent. """ import sys import os from typing import Dict, Any # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error, normalize_path from features.file_operations.model_tools import ToolUseRequest from features.file_operations.file_editor import FileEditor def handle_tool_use(input_data: Dict[str, Any]) -> Dict[str, Any]: """ Handle tool use requests from the Claude agent. Args: input_data: The tool use request data from Claude Returns: Dictionary with the result or error message """ log_info("tool_handler", f"Received tool use request: {input_data}") try: # Parse the tool use request request = ToolUseRequest.from_dict(input_data) # Normalize the path path = normalize_path(request.path) if request.path else None # Handle the command if request.command == "view": start_line = request.kwargs.get("start_line") end_line = request.kwargs.get("end_line") if start_line is not None: start_line = int(start_line) if end_line is not None: end_line = int(end_line) result = FileEditor.read(path, start_line, end_line) elif request.command == "edit": old_str = request.kwargs.get("old_str", "") new_str = request.kwargs.get("new_str", "") result = FileEditor.edit_file(path, old_str, new_str) elif request.command == "create": content = request.kwargs.get("content", "") result = FileEditor.create_file(path, content) elif request.command == "insert": line_num = int(request.kwargs.get("line_num", 1)) content = request.kwargs.get("content", "") result = FileEditor.insert_line(path, line_num, content) else: log_error("tool_handler", f"Unknown command: {request.command}") return {"error": f"Unknown command: {request.command}"} # Return the result if result.success: return {"result": result.content or result.message} else: return {"error": result.message} except Exception as e: error_msg = f"Error handling tool use: {str(e)}" log_error("tool_handler", error_msg) return {"error": error_msg} ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2/write_tool.py ================================================ #!/usr/bin/env python3 """ Write tool for the Vertical Slice Architecture implementation of the file editor agent. This module provides file writing capabilities. """ import sys import os # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.file_operations.model_tools import FileOperationResult def write_file(path: str, content: str) -> FileOperationResult: """ Write content to a file. Args: path: The path to the file to write content: The content to write to the file Returns: FileOperationResult with result or error message """ log_info("write_tool", f"Writing to file {path}") try: with open(path, 'w', encoding='utf-8') as f: f.write(content) log_info("write_tool", f"Successfully wrote to file {path}") return FileOperationResult(success=True, content="", message=f"Successfully wrote to file {path}") except Exception as e: error_msg = f"Failed to write to file {path}: {str(e)}" log_error("write_tool", error_msg) return FileOperationResult(success=False, content="", message=error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2_gemini/__init__.py ================================================ ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2_gemini/api_tools.py ================================================ #!/usr/bin/env python3 """ API layer for file operations in the Vertical Slice Architecture. """ import os import sys import traceback from typing import Dict, Any, Optional, List, Union # Add the project root to the Python path sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../'))) from shared.utils import console from features.file_operations.service import FileOperationService from features.file_operations.model import ToolUseRequest, FileOperationResult class FileOperationsAPI: """ API for file operations. """ @staticmethod def handle_tool_use(tool_use: Dict[str, Any]) -> Dict[str, Any]: """ Handle text editor tool use from Claude. Args: tool_use: The tool use request from Claude Returns: Dictionary with result or error to send back to Claude """ try: # Convert the tool use dictionary to a ToolUseRequest object request = ToolUseRequest.from_dict(tool_use) console.log(f"[handle_tool_use] Received command: {request.command}, path: {request.path}") if not request.command: error_msg = "No command specified in tool use request" console.log(f"[handle_tool_use] Error: {error_msg}") return {"error": error_msg} if not request.path and request.command != "undo_edit": # undo_edit might not need a path error_msg = "No path specified in tool use request" console.log(f"[handle_tool_use] Error: {error_msg}") return {"error": error_msg} # The path normalization is now handled in each file operation function console.print(f"[blue]Executing {request.command} command on {request.path}[/blue]") result = None if request.command == "view": view_range = request.kwargs.get("view_range") console.log( f"[handle_tool_use] Calling view_file with view_range: {view_range}" ) result = FileOperationService.view_file(request.path, view_range) elif request.command == "str_replace": old_str = request.kwargs.get("old_str") new_str = request.kwargs.get("new_str") console.log(f"[handle_tool_use] Calling str_replace") result = FileOperationService.str_replace(request.path, old_str, new_str) elif request.command == "create": file_text = request.kwargs.get("file_text") console.log(f"[handle_tool_use] Calling create_file") result = FileOperationService.create_file(request.path, file_text) elif request.command == "insert": insert_line = request.kwargs.get("insert_line") new_str = request.kwargs.get("new_str") console.log(f"[handle_tool_use] Calling insert_text at line: {insert_line}") result = FileOperationService.insert_text(request.path, insert_line, new_str) elif request.command == "undo_edit": console.log(f"[handle_tool_use] Calling undo_edit") result = FileOperationService.undo_edit(request.path) else: error_msg = f"Unknown command: {request.command}" console.print(f"[red]{error_msg}[/red]") console.log(f"[handle_tool_use] Error: {error_msg}") return {"error": error_msg} # Convert the result to a dictionary if result.success: return {"result": result.data if result.data is not None else result.message} else: return {"error": result.message} except Exception as e: error_msg = f"Error handling tool use: {str(e)}" console.print(f"[red]{error_msg}[/red]") console.log(f"[handle_tool_use] Error: {str(e)}") console.log(traceback.format_exc()) return {"error": error_msg} ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2_gemini/create_tool.py ================================================ #!/usr/bin/env python3 """ Create tool for the Vertical Slice Architecture implementation of the file editor agent. This module provides file creation capabilities. """ import sys import os # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.file_operations.model_tools import FileOperationResult from features.file_operations.write_tool import write_file def create_file(path: str, content: str) -> FileOperationResult: """ Create a new file with the specified content. Args: path: The path to the file to create content: The content to write to the file Returns: FileOperationResult with result or error message """ log_info("create_tool", f"Creating file {path}") try: # Create directory if it doesn't exist os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True) # Use the write_file function to create the file return write_file(path, content) except Exception as e: error_msg = f"Failed to create file {path}: {str(e)}" log_error("create_tool", error_msg) return FileOperationResult(success=False, content="", message=error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2_gemini/file_agent.py ================================================ #!/usr/bin/env python3 """ File agent for the Vertical Slice Architecture implementation of the file editor agent. This module provides the agent interface for file operations. """ import time from typing import Tuple, Dict, Any, List, Optional, Callable from rich.console import Console from rich.panel import Panel from anthropic import Anthropic import sys import os # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error, display_token_usage from features.file_operations.model_tools import FileOperationResult from features.file_operations.tool_handler import handle_tool_use # Initialize rich console console = Console() # Define constants MODEL = "claude-3-7-sonnet-20250219" DEFAULT_THINKING_TOKENS = 3000 class FileAgent: """ File agent that provides an interface for AI-assisted file operations. """ @staticmethod def run_agent( client: Anthropic, prompt: str, max_thinking_tokens: int = DEFAULT_THINKING_TOKENS, max_loops: int = 10, use_token_efficiency: bool = False, ) -> Tuple[str, int, int]: """ Run the Claude agent with file editing capabilities. Args: client: The Anthropic client prompt: The user's prompt max_thinking_tokens: Maximum tokens for thinking max_loops: Maximum number of tool use loops use_token_efficiency: Whether to use token-efficient tool use beta feature Returns: Tuple containing: - Final response from Claude (str) - Total input tokens used (int) - Total output tokens used (int) """ # Track token usage input_tokens_total = 0 output_tokens_total = 0 system_prompt = """You are a helpful AI assistant with text editing capabilities. You have access to a text editor tool that can view, edit, and create files. Always think step by step about what you need to do before taking any action. Be careful when making edits to files, as they can permanently change the user's files. Follow these steps when handling file operations: 1. First, view files to understand their content before making changes 2. For edits, ensure you have the correct context and are making the right changes 3. When creating files, make sure they're in the right location with proper formatting """ # Define text editor tool text_editor_tool = {"name": "str_replace_editor", "type": "text_editor_20250124"} messages = [ { "role": "user", "content": f"""I need help with editing files. Here's what I want to do: {prompt} Please use the text editor tool to help me with this. First, think through what you need to do, then use the appropriate tool. """, } ] loop_count = 0 tool_use_count = 0 thinking_start_time = time.time() while loop_count < max_loops: loop_count += 1 console.rule(f"[yellow]Agent Loop {loop_count}/{max_loops}[/yellow]") log_info("file_agent", f"Starting agent loop {loop_count}/{max_loops}") # Create message with text editor tool message_args = { "model": MODEL, "max_tokens": 4096, "tools": [text_editor_tool], "messages": messages, "system": system_prompt, "thinking": {"type": "enabled", "budget_tokens": max_thinking_tokens}, } # Use the beta.messages with betas parameter if token efficiency is enabled if use_token_efficiency: # Using token-efficient tools beta feature message_args["betas"] = ["token-efficient-tools-2025-02-19"] response = client.beta.messages.create(**message_args) else: # Standard approach response = client.messages.create(**message_args) # Track token usage if hasattr(response, "usage"): input_tokens = getattr(response.usage, "input_tokens", 0) output_tokens = getattr(response.usage, "output_tokens", 0) input_tokens_total += input_tokens output_tokens_total += output_tokens console.print( f"[dim]Loop {loop_count} tokens: Input={input_tokens}, Output={output_tokens}[/dim]" ) log_info( "file_agent", f"Loop {loop_count} tokens: Input={input_tokens}, Output={output_tokens}" ) # Process response content thinking_block = None tool_use_block = None text_block = None for content_block in response.content: if content_block.type == "thinking": thinking_block = content_block # Access the thinking attribute which contains the actual thinking text if hasattr(thinking_block, "thinking"): console.print( Panel( thinking_block.thinking, title=f"Claude's Thinking (Loop {loop_count})", border_style="blue", ) ) else: console.print( Panel( "Claude is thinking...", title=f"Claude's Thinking (Loop {loop_count})", border_style="blue", ) ) elif content_block.type == "tool_use": tool_use_block = content_block tool_use_count += 1 elif content_block.type == "text": text_block = content_block # If we got a final text response with no tool use, we're done if text_block and not tool_use_block: thinking_end_time = time.time() thinking_duration = thinking_end_time - thinking_start_time console.print( f"\n[bold green]Completed in {thinking_duration:.2f} seconds after {loop_count} loops and {tool_use_count} tool uses[/bold green]" ) log_info( "file_agent", f"Completed in {thinking_duration:.2f} seconds after {loop_count} loops and {tool_use_count} tool uses" ) # Add the response to messages messages.append( { "role": "assistant", "content": [ *([thinking_block] if thinking_block else []), {"type": "text", "text": text_block.text}, ], } ) return text_block.text, input_tokens_total, output_tokens_total # Handle tool use if tool_use_block: # Add the assistant's response to messages before handling tool calls messages.append({"role": "assistant", "content": response.content}) console.print( f"\n[bold blue]Tool Call:[/bold blue] {tool_use_block.name}" ) log_info("file_agent", f"Tool Call: {tool_use_block.name}") # Handle the tool use with our handler tool_result = handle_tool_use(tool_use_block.input) # Format tool result for Claude tool_result_message = { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_use_block.id, "content": tool_result.get("error") or tool_result.get("result", ""), } ], } messages.append(tool_result_message) # If we reach here, we hit the max loops console.print( f"\n[bold red]Warning: Reached maximum loops ({max_loops}) without completing the task[/bold red]" ) log_error( "file_agent", f"Reached maximum loops ({max_loops}) without completing the task" ) return ( "I wasn't able to complete the task within the allowed number of thinking steps. Please try a more specific prompt or increase the loop limit.", input_tokens_total, output_tokens_total, ) # Expose the run_agent function at the module level def run_agent( client: Anthropic, prompt: str, max_tool_use_loops: int = 15, token_efficient_tool_use: bool = True, ) -> Tuple[int, int]: """ Run the file editor agent with the specified prompt. Args: client: The Anthropic client prompt: The prompt to send to Claude max_tool_use_loops: Maximum number of tool use loops token_efficient_tool_use: Whether to use token-efficient tool use Returns: Tuple containing input and output token counts """ log_info("file_agent", f"Running agent with prompt: {prompt}") _, input_tokens, output_tokens = FileAgent.run_agent( client=client, prompt=prompt, max_loops=max_tool_use_loops, use_token_efficiency=token_efficient_tool_use, max_thinking_tokens=DEFAULT_THINKING_TOKENS ) return input_tokens, output_tokens ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2_gemini/file_editor.py ================================================ #!/usr/bin/env python3 """ File editor for the Vertical Slice Architecture implementation of the file editor agent. This module combines reading and writing capabilities for file editing. """ import sys import os from typing import Dict, Any, Tuple, Optional # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.file_operations.model_tools import FileOperationResult from features.file_operations.file_writer import FileWriter from features.file_operations.read_tool import read_file class FileEditor: """ File editor that combines reading and writing capabilities for file editing. """ @staticmethod def read(path: str, start_line: Optional[int] = None, end_line: Optional[int] = None) -> FileOperationResult: """ Read the contents of a file. Args: path: The path to the file to read start_line: Optional start line (1-indexed) end_line: Optional end line (1-indexed, -1 for end of file) Returns: FileOperationResult with content or error message """ log_info("file_editor", f"Reading file {path} with range {start_line}-{end_line}") return read_file(path, start_line, end_line) @staticmethod def view_file(path: str, view_range=None) -> FileOperationResult: """ View the contents of a file with optional range. Args: path: The path to the file to view view_range: Optional tuple of (start_line, end_line) Returns: FileOperationResult with content or error message """ start_line = None end_line = None if view_range: start_line, end_line = view_range log_info("file_editor", f"Viewing file {path} with range {start_line}-{end_line}") return FileEditor.read(path, start_line, end_line) @staticmethod def edit_file(path: str, old_str: str, new_str: str) -> FileOperationResult: """ Edit a file by replacing one string with another. Args: path: The path to the file to edit old_str: The string to replace new_str: The string to replace it with Returns: FileOperationResult with result or error message """ log_info("file_editor", f"Editing file {path}") # First, read the file to check if it exists read_result = FileEditor.read(path) if not read_result.success: log_error("file_editor", f"Cannot edit file that can't be read: {read_result.message}") return read_result # Then, use the file writer to replace the string return FileWriter.replace(path, old_str, new_str) @staticmethod def create_file(path: str, content: str) -> FileOperationResult: """ Create a new file with the specified content. Args: path: The path to the file to create content: The content for the new file Returns: FileOperationResult with result or error message """ log_info("file_editor", f"Creating file {path}") # Use the file writer to create the file return FileWriter.create(path, content) @staticmethod def insert_line(path: str, line_num: int, content: str) -> FileOperationResult: """ Insert content at a specific line in a file. Args: path: The path to the file to modify line_num: The line number where to insert (1-indexed) content: The content to insert Returns: FileOperationResult with result or error message """ log_info("file_editor", f"Inserting at line {line_num} in file {path}") # First, read the file to check if it exists read_result = FileEditor.read(path) if not read_result.success: log_error("file_editor", f"Cannot modify file that can't be read: {read_result.message}") return read_result # Then, use the file writer to insert the line return FileWriter.insert(path, line_num, content) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2_gemini/file_writer.py ================================================ #!/usr/bin/env python3 """ File writer for the Vertical Slice Architecture implementation of the file editor agent. This module provides file writing capabilities by composing various tools. """ import sys import os from typing import Dict, Any # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.file_operations.model_tools import FileOperationResult from features.file_operations.write_tool import write_file from features.file_operations.replace_tool import replace_in_file from features.file_operations.insert_tool import insert_in_file from features.file_operations.create_tool import create_file class FileWriter: """ File writer that composes various tools to provide file writing capabilities. """ @staticmethod def write(path: str, content: str) -> FileOperationResult: """ Write content to a file. Args: path: The path to the file to write content: The content to write to the file Returns: FileOperationResult with result or error message """ log_info("file_writer", f"Writing to file {path}") return write_file(path, content) @staticmethod def replace(path: str, old_str: str, new_str: str) -> FileOperationResult: """ Replace a string in a file. Args: path: The path to the file to modify old_str: The string to replace new_str: The string to replace with Returns: FileOperationResult with result or error message """ log_info("file_writer", f"Replacing text in file {path}") return replace_in_file(path, old_str, new_str) @staticmethod def insert(path: str, insert_line: int, new_str: str) -> FileOperationResult: """ Insert text at a specific line in a file. Args: path: The path to the file to modify insert_line: The line number after which to insert the text (1-indexed) new_str: The text to insert Returns: FileOperationResult with result or error message """ log_info("file_writer", f"Inserting text at line {insert_line} in file {path}") return insert_in_file(path, insert_line, new_str) @staticmethod def create(path: str, content: str) -> FileOperationResult: """ Create a new file with the specified content. Args: path: The path to the file to create content: The content to write to the file Returns: FileOperationResult with result or error message """ log_info("file_writer", f"Creating file {path}") return create_file(path, content) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2_gemini/insert_tool.py ================================================ #!/usr/bin/env python3 """ Insert tool for the Vertical Slice Architecture implementation of the file editor agent. This module provides line insertion capabilities for files. """ import sys import os # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.file_operations.model_tools import FileOperationResult def insert_in_file(path: str, insert_line: int, new_str: str) -> FileOperationResult: """ Insert text at a specific line in a file. Args: path: The path to the file to modify insert_line: The line number after which to insert the text (1-indexed) new_str: The text to insert Returns: FileOperationResult with result or error message """ log_info("insert_tool", f"Inserting text at line {insert_line} in file {path}") try: # Read the existing content with open(path, 'r', encoding='utf-8') as f: lines = f.readlines() if insert_line < 1 or insert_line > len(lines) + 1: error_msg = f"Invalid line number {insert_line} for file {path} with {len(lines)} lines" log_error("insert_tool", error_msg) return FileOperationResult(success=False, content="", message=error_msg) # Insert the new string at the specified position lines.insert(insert_line - 1, new_str if new_str.endswith('\n') else new_str + '\n') # Write the modified content back to the file with open(path, 'w', encoding='utf-8') as f: f.writelines(lines) log_info("insert_tool", f"Successfully inserted text at line {insert_line} in file {path}") return FileOperationResult(success=True, content="", message=f"Successfully inserted text at line {insert_line} in file {path}") except Exception as e: error_msg = f"Failed to insert text at line {insert_line} in file {path}: {str(e)}" log_error("insert_tool", error_msg) return FileOperationResult(success=False, content="", message=error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2_gemini/model_tools.py ================================================ #!/usr/bin/env python3 """ Models for the file operations feature in the Vertical Slice Architecture. """ import os import sys from typing import Dict, Any, Optional, List, Union # Add the project root to the Python path sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../'))) class FileOperationResult: """ Model representing the result of a file operation. """ def __init__(self, success: bool, message: str, content: str = "", data: Any = None): """ Initialize a file operation result. Args: success: Whether the operation was successful message: A message describing the result content: File content if the operation returns content data: Optional data returned by the operation """ self.success = success self.message = message self.content = content self.data = data def to_dict(self) -> Dict[str, Any]: """ Convert the result to a dictionary. Returns: Dictionary representation of the result """ return { "success": self.success, "message": self.message, "content": self.content, "data": self.data } class ToolUseRequest: """ Model representing a tool use request from Claude. """ def __init__(self, command: str, path: str = None, **kwargs): """ Initialize a tool use request. Args: command: The command to execute path: The path to operate on **kwargs: Additional arguments for the command """ self.command = command self.path = path self.kwargs = kwargs @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'ToolUseRequest': """ Create a tool use request from a dictionary. Args: data: Dictionary containing the tool use request Returns: A ToolUseRequest instance """ command = data.get("command") path = data.get("path") # Extract all other keys as kwargs kwargs = {k: v for k, v in data.items() if k not in ["command", "path"]} return cls(command, path, **kwargs) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2_gemini/read_tool.py ================================================ #!/usr/bin/env python3 """ Read tool for the Vertical Slice Architecture implementation of the file editor agent. This module provides file reading capabilities. """ import sys import os from typing import Optional # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.file_operations.model_tools import FileOperationResult def read_file(path: str, start_line: Optional[int] = None, end_line: Optional[int] = None) -> FileOperationResult: """ Read the contents of a file. Args: path: The path to the file to read start_line: Optional start line (1-indexed) end_line: Optional end line (1-indexed, -1 for end of file) Returns: FileOperationResult with content or error message """ log_info("read_tool", f"Reading file {path} with range {start_line}-{end_line}") try: with open(path, 'r', encoding='utf-8') as f: all_lines = f.readlines() # Handle line range if start_line is not None: start_idx = max(0, start_line - 1) # Convert 1-indexed to 0-indexed else: start_idx = 0 if end_line is not None: if end_line == -1: end_idx = len(all_lines) else: end_idx = min(end_line, len(all_lines)) else: end_idx = len(all_lines) selected_lines = all_lines[start_idx:end_idx] content = ''.join(selected_lines) log_info("read_tool", f"Successfully read file {path}") return FileOperationResult(success=True, content=content, message=f"Successfully read file {path}") except Exception as e: error_msg = f"Failed to read file {path}: {str(e)}" log_error("read_tool", error_msg) return FileOperationResult(success=False, content="", message=error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2_gemini/replace_tool.py ================================================ #!/usr/bin/env python3 """ Replace tool for the Vertical Slice Architecture implementation of the file editor agent. This module provides string replacement capabilities for files. """ import sys import os # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.file_operations.model_tools import FileOperationResult def replace_in_file(path: str, old_str: str, new_str: str) -> FileOperationResult: """ Replace a string in a file. Args: path: The path to the file to modify old_str: The string to replace new_str: The string to replace with Returns: FileOperationResult with result or error message """ log_info("replace_tool", f"Replacing text in file {path}") try: # Read the existing content with open(path, 'r', encoding='utf-8') as f: content = f.read() # Count occurrences to verify uniqueness occurrences = content.count(old_str) if occurrences == 0: error_msg = f"String not found in file {path}" log_error("replace_tool", error_msg) return FileOperationResult(success=False, content="", message=error_msg) if occurrences > 1: error_msg = f"Multiple occurrences ({occurrences}) of the string found in file {path}. Need a unique string to replace." log_error("replace_tool", error_msg) return FileOperationResult(success=False, content="", message=error_msg) # Replace the string and write back to the file new_content = content.replace(old_str, new_str, 1) with open(path, 'w', encoding='utf-8') as f: f.write(new_content) log_info("replace_tool", f"Successfully replaced text in file {path}") return FileOperationResult(success=True, content="", message=f"Successfully replaced text in file {path}") except Exception as e: error_msg = f"Failed to replace text in file {path}: {str(e)}" log_error("replace_tool", error_msg) return FileOperationResult(success=False, content="", message=error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2_gemini/service_tools.py ================================================ #!/usr/bin/env python3 """ Service layer for file operations in the Vertical Slice Architecture. """ import os import sys import traceback from typing import Dict, Any, Optional, List, Tuple, Union # Add the project root to the Python path sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../'))) from shared.utils import console, normalize_path, display_file_content from features.file_operations.model import FileOperationResult class FileOperationService: """ Service for handling file operations. """ @staticmethod def view_file(path: str, view_range=None) -> FileOperationResult: """ View the contents of a file. Args: path: The path to the file to view view_range: Optional start and end lines to view [start, end] Returns: FileOperationResult with content or error message """ try: # Normalize the path path = normalize_path(path) if not os.path.exists(path): error_msg = f"File {path} does not exist" console.log(f"[view_file] Error: {error_msg}") return FileOperationResult(False, error_msg) with open(path, "r") as f: lines = f.readlines() if view_range: start, end = view_range # Convert to 0-indexed for Python start = max(0, start - 1) if end == -1: end = len(lines) else: end = min(len(lines), end) lines = lines[start:end] content = "".join(lines) # Display the file content (only for console, not returned to Claude) display_file_content(path, content) return FileOperationResult(True, f"Successfully viewed file {path}", content) except Exception as e: error_msg = f"Error viewing file: {str(e)}" console.print(f"[red]{error_msg}[/red]") console.log(f"[view_file] Error: {str(e)}") console.log(traceback.format_exc()) return FileOperationResult(False, error_msg) @staticmethod def str_replace(path: str, old_str: str, new_str: str) -> FileOperationResult: """ Replace a specific string in a file. Args: path: The path to the file to modify old_str: The text to replace new_str: The new text to insert Returns: FileOperationResult with result or error message """ try: # Normalize the path path = normalize_path(path) if not os.path.exists(path): error_msg = f"File {path} does not exist" console.log(f"[str_replace] Error: {error_msg}") return FileOperationResult(False, error_msg) with open(path, "r") as f: content = f.read() if old_str not in content: error_msg = f"The specified string was not found in the file {path}" console.log(f"[str_replace] Error: {error_msg}") return FileOperationResult(False, error_msg) new_content = content.replace(old_str, new_str, 1) with open(path, "w") as f: f.write(new_content) console.print(f"[green]Successfully replaced text in {path}[/green]") console.log(f"[str_replace] Successfully replaced text in {path}") return FileOperationResult(True, f"Successfully replaced text in {path}") except Exception as e: error_msg = f"Error replacing text: {str(e)}" console.print(f"[red]{error_msg}[/red]") console.log(f"[str_replace] Error: {str(e)}") console.log(traceback.format_exc()) return FileOperationResult(False, error_msg) @staticmethod def create_file(path: str, file_text: str) -> FileOperationResult: """ Create a new file with specified content. Args: path: The path where the new file should be created file_text: The content to write to the new file Returns: FileOperationResult with result or error message """ try: # Check if the path is empty or invalid if not path or not path.strip(): error_msg = "Invalid file path provided: path is empty." console.log(f"[create_file] Error: {error_msg}") return FileOperationResult(False, error_msg) # Normalize the path path = normalize_path(path) # Check if the directory exists directory = os.path.dirname(path) if directory and not os.path.exists(directory): console.log(f"[create_file] Creating directory: {directory}") os.makedirs(directory) with open(path, "w") as f: f.write(file_text or "") console.print(f"[green]Successfully created file {path}[/green]") console.log(f"[create_file] Successfully created file {path}") return FileOperationResult(True, f"Successfully created file {path}") except Exception as e: error_msg = f"Error creating file: {str(e)}" console.print(f"[red]{error_msg}[/red]") console.log(f"[create_file] Error: {str(e)}") console.log(traceback.format_exc()) return FileOperationResult(False, error_msg) @staticmethod def insert_text(path: str, insert_line: int, new_str: str) -> FileOperationResult: """ Insert text at a specific location in a file. Args: path: The path to the file to modify insert_line: The line number after which to insert the text new_str: The text to insert Returns: FileOperationResult with result or error message """ try: if not path or not path.strip(): error_msg = "Invalid file path provided: path is empty." console.log(f"[insert_text] Error: {error_msg}") return FileOperationResult(False, error_msg) # Normalize the path path = normalize_path(path) if not os.path.exists(path): error_msg = f"File {path} does not exist" console.log(f"[insert_text] Error: {error_msg}") return FileOperationResult(False, error_msg) if insert_line is None: error_msg = "No line number specified: insert_line is missing." console.log(f"[insert_text] Error: {error_msg}") return FileOperationResult(False, error_msg) with open(path, "r") as f: lines = f.readlines() # Line is 0-indexed for this function, but Claude provides 1-indexed insert_line = min(max(0, insert_line - 1), len(lines)) # Check that the index is within acceptable bounds if insert_line < 0 or insert_line > len(lines): error_msg = ( f"Insert line number {insert_line} out of range (0-{len(lines)})." ) console.log(f"[insert_text] Error: {error_msg}") return FileOperationResult(False, error_msg) # Ensure new_str ends with newline if new_str and not new_str.endswith("\n"): new_str += "\n" lines.insert(insert_line, new_str) with open(path, "w") as f: f.writelines(lines) console.print( f"[green]Successfully inserted text at line {insert_line + 1} in {path}[/green]" ) console.log( f"[insert_text] Successfully inserted text at line {insert_line + 1} in {path}" ) return FileOperationResult( True, f"Successfully inserted text at line {insert_line + 1} in {path}" ) except Exception as e: error_msg = f"Error inserting text: {str(e)}" console.print(f"[red]{error_msg}[/red]") console.log(f"[insert_text] Error: {str(e)}") console.log(traceback.format_exc()) return FileOperationResult(False, error_msg) @staticmethod def undo_edit(path: str) -> FileOperationResult: """ Placeholder for undo_edit functionality. In a real implementation, you would need to track edit history. Args: path: The path to the file whose last edit should be undone Returns: FileOperationResult with message about undo functionality """ try: if not path or not path.strip(): error_msg = "Invalid file path provided: path is empty." console.log(f"[undo_edit] Error: {error_msg}") return FileOperationResult(False, error_msg) # Normalize the path path = normalize_path(path) message = "Undo functionality is not implemented in this version." console.print(f"[yellow]{message}[/yellow]") console.log(f"[undo_edit] {message}") return FileOperationResult(True, message) except Exception as e: error_msg = f"Error in undo_edit: {str(e)}" console.print(f"[red]{error_msg}[/red]") console.log(f"[undo_edit] Error: {str(e)}") console.log(traceback.format_exc()) return FileOperationResult(False, error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2_gemini/tool_handler.py ================================================ #!/usr/bin/env python3 """ Tool handler for the Vertical Slice Architecture implementation of the file editor agent. This module handles tool use requests from the Claude agent. """ import sys import os from typing import Dict, Any # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error, normalize_path from features.file_operations.model_tools import ToolUseRequest from features.file_operations.file_editor import FileEditor def handle_tool_use(input_data: Dict[str, Any]) -> Dict[str, Any]: """ Handle tool use requests from the Claude agent. Args: input_data: The tool use request data from Claude Returns: Dictionary with the result or error message """ log_info("tool_handler", f"Received tool use request: {input_data}") try: # Parse the tool use request request = ToolUseRequest.from_dict(input_data) # Normalize the path path = normalize_path(request.path) if request.path else None # Handle the command if request.command == "view": start_line = request.kwargs.get("start_line") end_line = request.kwargs.get("end_line") if start_line is not None: start_line = int(start_line) if end_line is not None: end_line = int(end_line) result = FileEditor.read(path, start_line, end_line) elif request.command == "edit": old_str = request.kwargs.get("old_str", "") new_str = request.kwargs.get("new_str", "") result = FileEditor.edit_file(path, old_str, new_str) elif request.command == "create": content = request.kwargs.get("content", "") result = FileEditor.create_file(path, content) elif request.command == "insert": line_num = int(request.kwargs.get("line_num", 1)) content = request.kwargs.get("content", "") result = FileEditor.insert_line(path, line_num, content) else: log_error("tool_handler", f"Unknown command: {request.command}") return {"error": f"Unknown command: {request.command}"} # Return the result if result.success: return {"result": result.content or result.message} else: return {"error": result.message} except Exception as e: error_msg = f"Error handling tool use: {str(e)}" log_error("tool_handler", error_msg) return {"error": error_msg} ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/features/file_agent_v2_gemini/write_tool.py ================================================ #!/usr/bin/env python3 """ Write tool for the Vertical Slice Architecture implementation of the file editor agent. This module provides file writing capabilities. """ import sys import os # Add the parent directory to the Python path to enable relative imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from shared.utils import log_info, log_error from features.file_operations.model_tools import FileOperationResult def write_file(path: str, content: str) -> FileOperationResult: """ Write content to a file. Args: path: The path to the file to write content: The content to write to the file Returns: FileOperationResult with result or error message """ log_info("write_tool", f"Writing to file {path}") try: with open(path, 'w', encoding='utf-8') as f: f.write(content) log_info("write_tool", f"Successfully wrote to file {path}") return FileOperationResult(success=True, content="", message=f"Successfully wrote to file {path}") except Exception as e: error_msg = f"Failed to write to file {path}: {str(e)}" log_error("write_tool", error_msg) return FileOperationResult(success=False, content="", message=error_msg) ================================================ FILE: example-agent-codebase-arch/vertical-slice-architecture/main.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # "anthropic>=0.49.0", # "rich>=13.7.0", # ] # /// """ Main application entry point for the Vertical Slice Architecture implementation of the Claude 3.7 File Editor Agent. Example Usage: # View a file uv run main.py --prompt "Show me the content of README.md" # Edit a file uv run main.py --prompt "Fix the syntax error in sfa_poc.py" # Create a new file uv run main.py --prompt "Create a new file called hello.py with a function that prints Hello World" # Run with higher thinking tokens uv run main.py --prompt "Refactor README.md to make it more concise" --thinking 5000 # Increase max loops for complex tasks uv run main.py --prompt "Create a Python class that implements a binary search tree" --max-loops 20 """ import os import sys import argparse import time import traceback from typing import Tuple, Dict, Any from rich.console import Console from rich.panel import Panel from rich.markdown import Markdown # Add the current directory to the Python path to enable absolute imports sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from shared.utils import console, display_token_usage # Define constants MODEL = "claude-3-7-sonnet-20250219" DEFAULT_THINKING_TOKENS = 3000 def main(): """Main entry point for the application.""" # Set up argument parser parser = argparse.ArgumentParser(description="Claude 3.7 File Editor Agent") parser.add_argument( "--prompt", "-p", required=True, help="The prompt for what file operations to perform", ) parser.add_argument( "--max-loops", "-l", type=int, default=15, help="Maximum number of tool use loops (default: 15)", ) parser.add_argument( "--thinking", "-t", type=int, default=DEFAULT_THINKING_TOKENS, help=f"Maximum thinking tokens (default: {DEFAULT_THINKING_TOKENS})", ) parser.add_argument( "--efficiency", "-e", action="store_true", help="Enable token-efficient tool use (beta feature)", ) args = parser.parse_args() console.print(Panel.fit("Claude 3.7 File Editor Agent (Vertical Slice Architecture)")) console.print(f"\n[bold]Prompt:[/bold] {args.prompt}\n") console.print(f"[dim]Thinking tokens: {args.thinking}[/dim]") console.print(f"[dim]Max loops: {args.max_loops}[/dim]") if args.efficiency: console.print(f"[dim]Token-efficient tools: Enabled[/dim]\n") else: console.print(f"[dim]Token-efficient tools: Disabled[/dim]\n") # For testing purposes, we'll just print a success message console.print("[green]Successfully loaded the Vertical Slice Architecture implementation![/green]") console.print("[yellow]This is a mock implementation for testing the architecture structure.[/yellow]") console.print("[yellow]In a real implementation, this would connect to the Claude API.[/yellow]") # Display mock token usage display_token_usage(1000, 500) if __name__ == "__main__": main() ================================================ FILE: extra/ai_code_basic.sh ================================================ # aider --model groq/deepseek-r1-distill-llama-70b --no-detect-urls --no-auto-commit --yes-always --file *.py --message "$1" # aider --deepseek --no-detect-urls --no-auto-commit --yes-always --file *.py --message "$1" aider \ --model o3-mini \ --architect \ --reasoning-effort high \ --editor-model sonnet \ --no-detect-urls \ --no-auto-commit \ --yes-always \ --file *.py ================================================ FILE: extra/ai_code_reflect.sh ================================================ prompt="$1" # first shot aider \ --model o3-mini \ --architect \ --reasoning-effort high \ --editor-model sonnet \ --no-detect-urls \ --no-auto-commit \ --yes-always \ --file *.py \ --message "$prompt" # reflection aider \ --model o3-mini \ --architect \ --reasoning-effort high \ --editor-model sonnet \ --no-detect-urls \ --no-auto-commit \ --yes-always \ --file *.py \ --message "Double all changes requested to make sure they've been implemented: $prompt" ================================================ FILE: extra/create_db.py ================================================ import json import sqlite3 from datetime import datetime # Connect to SQLite database (creates it if it doesn't exist) conn = sqlite3.connect('users.db') cursor = conn.cursor() # Create the User table cursor.execute(''' CREATE TABLE IF NOT EXISTS User ( id TEXT PRIMARY KEY, name TEXT, age INTEGER, city TEXT, score REAL, is_active BOOLEAN, status TEXT, created_at DATE ) ''') # Read the JSON file with open('data/mock.json', 'r') as file: users = json.load(file) # Insert data into the table for user in users: cursor.execute(''' INSERT INTO User (id, name, age, city, score, is_active, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ''', ( user['id'], user['name'], user['age'], user['city'], user['score'], user['is_active'], user['status'], user['created_at'] )) # Commit the changes and close the connection conn.commit() conn.close() ================================================ FILE: extra/gist_poc.py ================================================ # /// script # dependencies = [ # "requests<3", # ] # /// # Interesting idea here - we can store SFAs in gist - curl them then run them locally. Food for thought. import requests def fetch_gist_content(): # 1. The raw link to your specific file in the Gist raw_url = "https://gist.githubusercontent.com/disler/d8d8abdb17b2072cff21df468607b176/raw/sfa_poc.py" try: # 2. Use requests to fetch the file's content response = requests.get(raw_url) response.raise_for_status() # Raise an exception for bad status codes # 3. Get the content sfa_poc_file_contents = response.text # 4. Print the content print(sfa_poc_file_contents) return sfa_poc_file_contents except requests.RequestException as e: print(f"Error fetching gist content: {e}") return None if __name__ == "__main__": fetch_gist_content() ================================================ FILE: extra/gist_poc.sh ================================================ #!/usr/bin/env bash # Interesting idea here - we can store SFAs in gist - curl them then run them locally. Food for thought. # 1. The raw link to your specific file in the Gist. # Note: The exact raw link may change if the Gist is updated, so check the "Raw" button # in your Gist to make sure you have the correct URL. RAW_URL="https://gist.githubusercontent.com/disler/d8d8abdb17b2072cff21df468607b176/raw/sfa_poc.py" # 2. Use curl to fetch the file's content and store it in a variable. SFA_POC_FILE_CONTENTS="$(curl -sL "$RAW_URL")" # 3. Now you can do whatever you want with $SFA_POC_FILE_CONTENTS. # For example, just echo it: echo "$SFA_POC_FILE_CONTENTS" ================================================ FILE: openai-agents-examples/01_basic_agent.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # "openai-agents>=0.0.2", # "pytest>=7.4.0", # "rich>=13.7.0", # ] # /// """ Basic Agent Example This example demonstrates how to create a simple agent using the OpenAI Agents SDK. The agent can respond to user queries with helpful information. Run with: uv run 01_basic_agent.py --prompt "Tell me about climate change" Test with: uv run pytest 01_basic_agent.py """ import os import sys import argparse import json from typing import Optional, List, Dict, Any, Union from rich.console import Console from rich.panel import Panel from openai import OpenAI from openai.types.chat import ChatCompletion from agents import Agent, Runner # Initialize console for rich output console = Console() def create_basic_agent(instructions: str = None) -> Agent: """ Create a basic agent with the given instructions. Args: instructions: Custom instructions for the agent. If None, default instructions are used. Returns: An Agent instance configured with the provided instructions. """ default_instructions = """ You are a helpful assistant that provides accurate and concise information. Always be respectful and provide factual responses based on the latest available information. If you don't know something, admit it rather than making up information. """ # Create and return a basic agent return Agent( name="BasicAssistant", instructions=instructions or default_instructions, model="gpt-4o-mini", # Using GPT-4o-mini as specified in requirements ) async def run_basic_agent(prompt: str, agent: Optional[Agent] = None) -> str: """ Run the basic agent with the given prompt. Args: prompt: The user's query or prompt agent: Optional pre-configured agent. If None, a default agent is created. Returns: The agent's response as a string """ # Create agent if not provided if agent is None: agent = create_basic_agent() # Run the agent with the prompt result = await Runner.run(agent, prompt) # Extract and return the text response return result.final_output def main(): """Main function to parse arguments and run the agent.""" parser = argparse.ArgumentParser(description="Basic Agent Example") parser.add_argument("--prompt", "-p", type=str, required=True, help="The prompt to send to the agent") args = parser.parse_args() # Ensure API key is available if not os.environ.get("OPENAI_API_KEY"): console.print(Panel("[bold red]Error: OPENAI_API_KEY environment variable not set[/bold red]")) sys.exit(1) try: # Run the agent and get response import asyncio response = asyncio.run(run_basic_agent(args.prompt)) # Display the response console.print(Panel(response, title="Agent Response", border_style="green")) except Exception as e: console.print(Panel(f"[bold red]Error: {str(e)}[/bold red]")) sys.exit(1) # Test functions def test_create_basic_agent(): """Test that the agent is created with the correct configuration.""" agent = create_basic_agent("Test instructions") assert agent.name == "BasicAssistant" assert agent.instructions == "Test instructions" assert agent.model == "gpt-4o-mini" def test_run_basic_agent(): """Test that the agent can run and produce a response.""" import pytest # Skip this test if no API key is available if not os.environ.get("OPENAI_API_KEY"): pytest.skip("OPENAI_API_KEY not set") # Run a simple test query import asyncio response = asyncio.run(run_basic_agent("What is 2+2?")) # Verify we got a non-empty response assert response assert len(response) > 0 # The response should contain "4" somewhere assert "4" in response if __name__ == "__main__": main() ================================================ FILE: openai-agents-examples/02_multi_agent.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # "openai-agents>=0.0.2", # "pytest>=7.4.0", # "rich>=13.7.0", # ] # /// """ Multi-Agent Example This example demonstrates how to create and use multiple agents that work together. It includes a coordinator agent that delegates tasks to specialist agents. Run with: uv run 02_multi_agent.py --prompt "Explain quantum computing and its applications" Test with: uv run pytest 02_multi_agent.py """ import os import sys import argparse import json import asyncio from typing import Optional, List, Dict, Any, Union, Tuple from rich.console import Console from rich.panel import Panel from openai import OpenAI from agents import Agent, Runner, handoff # Initialize console for rich output console = Console() def create_science_agent() -> Agent: """ Create a science specialist agent. Returns: An Agent instance specialized in scientific topics. """ instructions = """ You are a science specialist with deep knowledge of physics, chemistry, biology, and related fields. Provide accurate, detailed scientific explanations while making complex concepts accessible. Use analogies and examples when helpful to illustrate scientific principles. Always clarify when something is theoretical or not yet proven. """ return Agent( name="ScienceSpecialist", instructions=instructions, model="gpt-4o-mini", handoff_description="Use this agent for questions about scientific topics, theories, and concepts." ) def create_tech_agent() -> Agent: """ Create a technology specialist agent. Returns: An Agent instance specialized in technology topics. """ instructions = """ You are a technology specialist with expertise in computer science, programming, AI, and digital technologies. Provide clear, accurate explanations of technical concepts and their practical applications. When discussing programming, focus on concepts rather than writing extensive code. Explain how technologies work and their real-world impact. """ return Agent( name="TechSpecialist", instructions=instructions, model="gpt-4o-mini", handoff_description="Use this agent for questions about technology, computing, programming, and digital systems." ) def create_coordinator_agent(specialists: List[Agent]) -> Agent: """ Create a coordinator agent that can delegate to specialists. Args: specialists: List of specialist agents to which tasks can be delegated Returns: An Agent instance that coordinates between specialists """ instructions = """ You are a coordinator who determines which specialist should handle a user's question. Analyze the user's query and decide which specialist would be best suited to respond. For questions that span multiple domains, choose the specialist most relevant to the core of the question. """ # Create handoffs to specialist agents handoffs = [handoff(agent) for agent in specialists] return Agent( name="Coordinator", instructions=instructions, model="gpt-4o-mini", handoffs=handoffs ) async def run_multi_agent_system(prompt: str) -> str: """ Run the multi-agent system with the given prompt. Args: prompt: The user's query or prompt Returns: The final response from the appropriate specialist agent """ # Create specialist agents science_agent = create_science_agent() tech_agent = create_tech_agent() # Create coordinator agent with specialists coordinator = create_coordinator_agent([science_agent, tech_agent]) # Run the coordinator agent with the prompt result = await Runner.run(coordinator, prompt) # Return the final response return result.final_output def main(): """Main function to parse arguments and run the multi-agent system.""" parser = argparse.ArgumentParser(description="Multi-Agent Example") parser.add_argument("--prompt", "-p", type=str, required=True, help="The prompt to send to the multi-agent system") args = parser.parse_args() # Ensure API key is available if not os.environ.get("OPENAI_API_KEY"): console.print(Panel("[bold red]Error: OPENAI_API_KEY environment variable not set[/bold red]")) sys.exit(1) try: # Run the multi-agent system and get response response = asyncio.run(run_multi_agent_system(args.prompt)) # Display the response console.print(Panel(response, title="Multi-Agent Response", border_style="green")) except Exception as e: console.print(Panel(f"[bold red]Error: {str(e)}[/bold red]")) sys.exit(1) # Test functions def test_create_specialist_agents(): """Test that specialist agents are created with the correct configuration.""" science_agent = create_science_agent() tech_agent = create_tech_agent() assert science_agent.name == "ScienceSpecialist" assert tech_agent.name == "TechSpecialist" assert "science specialist" in science_agent.instructions.lower() assert "technology specialist" in tech_agent.instructions.lower() def test_create_coordinator_agent(): """Test that the coordinator agent is created with the correct configuration.""" science_agent = create_science_agent() tech_agent = create_tech_agent() coordinator = create_coordinator_agent([science_agent, tech_agent]) assert coordinator.name == "Coordinator" assert "coordinator" in coordinator.instructions.lower() assert len(coordinator.handoffs) == 2 def test_run_multi_agent_system(): """Test that the multi-agent system can run and produce a response.""" import pytest # Skip this test if no API key is available if not os.environ.get("OPENAI_API_KEY"): pytest.skip("OPENAI_API_KEY not set") # Run a simple test query that should go to the tech specialist response = asyncio.run(run_multi_agent_system("What is machine learning?")) # Verify we got a non-empty response assert response assert len(response) > 0 if __name__ == "__main__": main() ================================================ FILE: openai-agents-examples/03_sync_agent.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # "openai-agents>=0.0.2", # "pytest>=7.4.0", # "rich>=13.7.0", # ] # /// """ Synchronous Agent Example This example demonstrates how to run an agent synchronously instead of asynchronously. It shows how to use the Runner.run_sync function for simpler code in non-async environments. Run with: uv run 03_sync_agent.py --prompt "What are the benefits of exercise?" Test with: uv run pytest 03_sync_agent.py """ import os import sys import argparse import json from typing import Optional, List, Dict, Any, Union from rich.console import Console from rich.panel import Panel from openai import OpenAI from agents import Agent, Runner # Initialize console for rich output console = Console() def create_health_agent() -> Agent: """ Create a health advisor agent. Returns: An Agent instance specialized in health topics. """ instructions = """ You are a health advisor with expertise in fitness, nutrition, and general wellness. Provide evidence-based information about health topics, focusing on practical advice. Always emphasize that you're not a medical professional and serious concerns should be discussed with a healthcare provider. Keep responses concise and actionable. """ return Agent( name="HealthAdvisor", instructions=instructions, model="gpt-4o-mini", ) def run_sync_agent(prompt: str, agent: Optional[Agent] = None) -> str: """ Run an agent synchronously with the given prompt. Args: prompt: The user's query or prompt agent: Optional pre-configured agent. If None, a health advisor agent is created. Returns: The agent's response as a string """ # Create agent if not provided if agent is None: agent = create_health_agent() # Run the agent synchronously with the prompt result = Runner.run_sync(agent, prompt) # Return the response return result.final_output def main(): """Main function to parse arguments and run the agent synchronously.""" parser = argparse.ArgumentParser(description="Synchronous Agent Example") parser.add_argument("--prompt", "-p", type=str, required=True, help="The prompt to send to the agent") args = parser.parse_args() # Ensure API key is available if not os.environ.get("OPENAI_API_KEY"): console.print(Panel("[bold red]Error: OPENAI_API_KEY environment variable not set[/bold red]")) sys.exit(1) try: # Run the agent synchronously and get response response = run_sync_agent(args.prompt) # Display the response console.print(Panel(response, title="Synchronous Agent Response", border_style="green")) except Exception as e: console.print(Panel(f"[bold red]Error: {str(e)}[/bold red]")) sys.exit(1) # Test functions def test_create_health_agent(): """Test that the health agent is created with the correct configuration.""" agent = create_health_agent() assert agent.name == "HealthAdvisor" assert "health advisor" in agent.instructions.lower() assert agent.model == "gpt-4o-mini" def test_run_sync_agent(): """Test that the agent can run synchronously and produce a response.""" import pytest # Skip this test if no API key is available if not os.environ.get("OPENAI_API_KEY"): pytest.skip("OPENAI_API_KEY not set") # Run a simple test query response = run_sync_agent("What are some quick exercises I can do at my desk?") # Verify we got a non-empty response assert response assert len(response) > 0 # The response should contain relevant terms assert any(term in response.lower() for term in ["exercise", "stretch", "desk", "movement"]) if __name__ == "__main__": main() ================================================ FILE: openai-agents-examples/04_agent_with_tracing.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # "openai-agents>=0.0.2", # "pytest>=7.4.0", # "rich>=13.7.0", # "opentelemetry-api>=1.20.0", # "opentelemetry-sdk>=1.20.0", # ] # /// """ Agent with Tracing Example This example demonstrates how to use tracing with agents to monitor and debug their execution. It shows how to set up OpenTelemetry tracing and capture spans for agent operations. Run with: uv run 04_agent_with_tracing.py --prompt "What is the capital of France?" Test with: uv run pytest 04_agent_with_tracing.py """ import os import sys import argparse import json import asyncio from typing import Optional, List, Dict, Any, Union from rich.console import Console from rich.panel import Panel from openai import OpenAI from agents import Agent, Runner # Import OpenTelemetry components from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor # Initialize console for rich output console = Console() # Set up OpenTelemetry tracing def setup_tracing(): """Set up OpenTelemetry tracing with console exporter.""" # Create a tracer provider provider = TracerProvider() # Add a console exporter to see spans in the console console_exporter = ConsoleSpanExporter() processor = SimpleSpanProcessor(console_exporter) provider.add_span_processor(processor) # Set the global tracer provider trace.set_tracer_provider(provider) # Get a tracer return trace.get_tracer("agent_tracer") def create_geography_agent() -> Agent: """ Create a geography specialist agent. Returns: An Agent instance specialized in geography topics. """ instructions = """ You are a geography specialist with knowledge about countries, capitals, landmarks, and geographical features. Provide accurate, concise information about geographical topics. Include interesting facts when relevant but prioritize accuracy. """ return Agent( name="GeographySpecialist", instructions=instructions, model="gpt-4o-mini", ) async def run_traced_agent(prompt: str, tracer) -> str: """ Run an agent with tracing for the given prompt. Args: prompt: The user's query or prompt tracer: The OpenTelemetry tracer to use Returns: The agent's response as a string """ # Create a span for the entire agent execution with tracer.start_as_current_span("agent_execution") as span: # Add attributes to the span span.set_attribute("prompt", prompt) # Create the agent with tracer.start_as_current_span("create_agent"): agent = create_geography_agent() span.set_attribute("agent_name", agent.name) # Run the agent with the prompt with tracer.start_as_current_span("Runner.run"): result = await Runner.run(agent, prompt) # Note: In the current version, RunResult doesn't have usage attribute # We'll just record the response length as a basic metric span.set_attribute("response_length", len(result.final_output)) span.set_attribute("response_first_chars", result.final_output[:30]) # Return the response return result.final_output def main(): """Main function to parse arguments and run the agent with tracing.""" parser = argparse.ArgumentParser(description="Agent with Tracing Example") parser.add_argument("--prompt", "-p", type=str, required=True, help="The prompt to send to the agent") args = parser.parse_args() # Ensure API key is available if not os.environ.get("OPENAI_API_KEY"): console.print(Panel("[bold red]Error: OPENAI_API_KEY environment variable not set[/bold red]")) sys.exit(1) try: # Set up tracing tracer = setup_tracing() # Run the agent with tracing and get response response = asyncio.run(run_traced_agent(args.prompt, tracer)) # Display the response console.print(Panel(response, title="Agent Response with Tracing", border_style="green")) except Exception as e: console.print(Panel(f"[bold red]Error: {str(e)}[/bold red]")) sys.exit(1) # Test functions def test_create_geography_agent(): """Test that the geography agent is created with the correct configuration.""" agent = create_geography_agent() assert agent.name == "GeographySpecialist" assert "geography specialist" in agent.instructions.lower() assert agent.model == "gpt-4o-mini" def test_run_traced_agent(): """Test that the agent can run with tracing and produce a response.""" import pytest # Skip this test if no API key is available if not os.environ.get("OPENAI_API_KEY"): pytest.skip("OPENAI_API_KEY not set") # Set up tracing tracer = setup_tracing() # Run a simple test query response = asyncio.run(run_traced_agent("What is the capital of Japan?", tracer)) # Verify we got a non-empty response assert response assert len(response) > 0 # The response should contain "Tokyo" assert "Tokyo" in response if __name__ == "__main__": main() ================================================ FILE: openai-agents-examples/05_agent_with_function_tools.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # "openai-agents>=0.0.2", # "pytest>=7.4.0", # "rich>=13.7.0", # "requests>=2.31.0", # ] # /// """ Agent with Function Tools Example This example demonstrates how to create an agent with function tools using the @function_tool decorator. The agent can use these tools to perform actions like fetching weather data or calculating distances. Run with: uv run 05_agent_with_function_tools.py --prompt "What's the weather in New York?" Test with: uv run pytest 05_agent_with_function_tools.py """ import os import sys import argparse import json import asyncio import requests from datetime import datetime from typing import Optional, List, Dict, Any, Union from rich.console import Console from rich.panel import Panel from openai import OpenAI from agents import Agent, Runner, function_tool # Initialize console for rich output console = Console() # Define function tools using the decorator @function_tool def get_current_weather(location: str, unit: str) -> str: """ Get the current weather in a given location. Args: location: The city and state, e.g. San Francisco, CA or country e.g., London, UK unit: The temperature unit to use. Either "celsius" or "fahrenheit". Returns: A string containing the weather information. """ # This is a mock implementation - in a real application, you would call a weather API weather_data = { "New York": {"temperature": 22, "condition": "Sunny"}, "London": {"temperature": 15, "condition": "Cloudy"}, "Tokyo": {"temperature": 28, "condition": "Rainy"}, "Sydney": {"temperature": 31, "condition": "Hot and sunny"}, } # Default weather if location not found default_weather = {"temperature": 20, "condition": "Clear"} # Get weather for the location (case insensitive) location_key = next((k for k in weather_data.keys() if k.lower() == location.lower()), None) weather = weather_data.get(location_key, default_weather) # Convert temperature if needed temp = weather["temperature"] if unit.lower() == "fahrenheit": temp = (temp * 9/5) + 32 return f"The current weather in {location} is {weather['condition']} with a temperature of {temp}°{'F' if unit.lower() == 'fahrenheit' else 'C'}." @function_tool def calculate_distance(origin: str, destination: str, unit: str) -> str: """ Calculate the distance between two locations. Args: origin: The starting location (city name) destination: The ending location (city name) unit: The unit of distance. Either "kilometers" or "miles". Returns: A string containing the distance information. """ # This is a mock implementation - in a real application, you would call a mapping API distances = { ("New York", "London"): 5567, ("New York", "Tokyo"): 10838, ("London", "Tokyo"): 9562, ("London", "Sydney"): 16983, ("Tokyo", "Sydney"): 7921, } # Try to find the distance in both directions distance_km = distances.get((origin, destination)) or distances.get((destination, origin)) # If not found, provide an estimate if distance_km is None: distance_km = 1000 # Default distance # Convert to miles if needed if unit.lower() == "miles": distance = distance_km * 0.621371 unit_symbol = "miles" else: distance = distance_km unit_symbol = "km" return f"The distance between {origin} and {destination} is approximately {distance:.1f} {unit_symbol}." @function_tool def get_current_time(location: str) -> str: """ Get the current time in a given location. Args: location: The location to get the time for. Currently only supports "UTC". Returns: A string containing the current time information. """ # In a real implementation, you would use a timezone library current_time = datetime.utcnow() formatted_time = current_time.strftime("%Y-%m-%d %H:%M:%S") return f"The current time in {location} is {formatted_time}." def create_travel_assistant() -> Agent: """ Create a travel assistant agent with function tools. Returns: An Agent instance with function tools for travel assistance. """ instructions = """ You are a helpful travel assistant that can provide information about weather, distances between locations, and current time. Use the tools available to you to provide accurate information when asked. If you don't have a tool for the specific request, acknowledge the limitations and provide the best information you can. """ # Create the agent with function tools return Agent( name="TravelAssistant", instructions=instructions, model="gpt-4o-mini", tools=[get_current_weather, calculate_distance, get_current_time] ) async def run_function_tool_agent(prompt: str) -> str: """ Run the travel assistant agent with the given prompt. Args: prompt: The user's query or prompt Returns: The agent's response as a string """ # Create the agent with function tools agent = create_travel_assistant() # Run the agent with the prompt result = await Runner.run(agent, prompt) # Return the response return result.final_output def main(): """Main function to parse arguments and run the agent with function tools.""" parser = argparse.ArgumentParser(description="Agent with Function Tools Example") parser.add_argument("--prompt", "-p", type=str, required=True, help="The prompt to send to the agent") args = parser.parse_args() # Ensure API key is available if not os.environ.get("OPENAI_API_KEY"): console.print(Panel("[bold red]Error: OPENAI_API_KEY environment variable not set[/bold red]")) sys.exit(1) try: # Run the agent and get response response = asyncio.run(run_function_tool_agent(args.prompt)) # Display the response console.print(Panel(response, title="Travel Assistant Response", border_style="green")) except Exception as e: console.print(Panel(f"[bold red]Error: {str(e)}[/bold red]")) sys.exit(1) # Test functions def test_function_tools(): """Test that the function tools work correctly.""" # Test weather function weather_result = get_current_weather("New York", "celsius") assert "New York" in weather_result assert "°C" in weather_result # Test distance function distance_result = calculate_distance("New York", "London", "kilometers") assert "New York" in distance_result assert "London" in distance_result assert "km" in distance_result # Test time function time_result = get_current_time() assert "UTC" in time_result assert ":" in time_result # Time should contain colons def test_create_travel_assistant(): """Test that the travel assistant agent is created with the correct configuration.""" agent = create_travel_assistant() assert agent.name == "TravelAssistant" assert "travel assistant" in agent.instructions.lower() assert len(agent.tools) == 3 def test_run_function_tool_agent(): """Test that the agent can use function tools and produce a response.""" import pytest # Skip this test if no API key is available if not os.environ.get("OPENAI_API_KEY"): pytest.skip("OPENAI_API_KEY not set") # Run a test query that should use the weather tool response = asyncio.run(run_function_tool_agent("What's the weather in London?")) # Verify we got a non-empty response that mentions London assert response assert len(response) > 0 assert "London" in response if __name__ == "__main__": main() ================================================ FILE: openai-agents-examples/06_agent_with_custom_tools.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # "openai-agents>=0.0.2", # "pytest>=7.4.0", # "rich>=13.7.0", # "pydantic>=2.0.0", # ] # /// """ Agent with Custom Tools Example This example demonstrates how to create an agent with custom tools without using the @function_tool decorator. It shows how to define custom tool schemas and implement tool handlers manually. Run with: uv run 06_agent_with_custom_tools.py --prompt "Convert 100 USD to EUR" Test with: uv run pytest 06_agent_with_custom_tools.py """ import os import sys import argparse import json import asyncio from typing import Optional, List, Dict, Any, Union, Callable from rich.console import Console from rich.panel import Panel from pydantic import BaseModel, Field from openai import OpenAI from agents import Agent, Runner, Tool # Initialize console for rich output console = Console() # Define custom tool input models class CurrencyConversionInput(BaseModel): """Input for currency conversion tool.""" amount: float = Field(..., description="The amount to convert") from_currency: str = Field(..., description="The currency to convert from (e.g., USD, EUR, JPY)") to_currency: str = Field(..., description="The currency to convert to (e.g., USD, EUR, JPY)") class StockPriceInput(BaseModel): """Input for stock price tool.""" symbol: str = Field(..., description="The stock symbol (e.g., AAPL, MSFT, GOOGL)") # Define custom tool handlers def convert_currency(params: CurrencyConversionInput) -> str: """ Convert an amount from one currency to another. Args: params: The currency conversion parameters Returns: A string containing the conversion result """ # This is a mock implementation - in a real application, you would call a currency API exchange_rates = { "USD": {"EUR": 0.92, "GBP": 0.79, "JPY": 149.50}, "EUR": {"USD": 1.09, "GBP": 0.86, "JPY": 162.50}, "GBP": {"USD": 1.27, "EUR": 1.16, "JPY": 189.20}, "JPY": {"USD": 0.0067, "EUR": 0.0062, "GBP": 0.0053}, } from_curr = params.from_currency.upper() to_curr = params.to_currency.upper() # Check if currencies are supported if from_curr not in exchange_rates: return f"Sorry, {from_curr} is not a supported currency." if to_curr not in exchange_rates[from_curr] and from_curr != to_curr: return f"Sorry, conversion from {from_curr} to {to_curr} is not supported." # If same currency, return the amount if from_curr == to_curr: return f"{params.amount} {from_curr} is equal to {params.amount} {to_curr}." # Calculate converted amount converted_amount = params.amount * exchange_rates[from_curr][to_curr] return f"{params.amount} {from_curr} is equal to {converted_amount:.2f} {to_curr}." def get_stock_price(params: StockPriceInput) -> str: """ Get the current price of a stock. Args: params: The stock price parameters Returns: A string containing the stock price information """ # This is a mock implementation - in a real application, you would call a stock API stock_prices = { "AAPL": 175.34, "MSFT": 410.34, "GOOGL": 147.68, "AMZN": 178.75, "META": 474.99, } symbol = params.symbol.upper() # Check if stock is supported if symbol not in stock_prices: return f"Sorry, stock information for {symbol} is not available." price = stock_prices[symbol] return f"The current price of {symbol} is ${price:.2f}." def create_financial_assistant() -> Agent: """ Create a financial assistant agent with custom tools. Returns: An Agent instance with custom tools for financial assistance """ instructions = """ You are a helpful financial assistant that can provide information about currency conversions and stock prices. Use the tools available to you to provide accurate financial information when asked. If you don't have a tool for the specific request, acknowledge the limitations and provide the best information you can. """ # Create custom tools currency_tool = Tool( name="convert_currency", description="Convert an amount from one currency to another", input_type=CurrencyConversionInput, function=convert_currency ) stock_tool = Tool( name="get_stock_price", description="Get the current price of a stock", input_type=StockPriceInput, function=get_stock_price ) # Create the agent with custom tools return Agent( name="FinancialAssistant", instructions=instructions, model="gpt-4o-mini", tools=[currency_tool, stock_tool] ) async def run_custom_tool_agent(prompt: str) -> str: """ Run the financial assistant agent with the given prompt. Args: prompt: The user's query or prompt Returns: The agent's response as a string """ # Create the agent with custom tools agent = create_financial_assistant() # Run the agent with the prompt result = await Runner.run(agent, prompt) # Return the response return result.final_output def main(): """Main function to parse arguments and run the agent with custom tools.""" parser = argparse.ArgumentParser(description="Agent with Custom Tools Example") parser.add_argument("--prompt", "-p", type=str, required=True, help="The prompt to send to the agent") args = parser.parse_args() # Ensure API key is available if not os.environ.get("OPENAI_API_KEY"): console.print(Panel("[bold red]Error: OPENAI_API_KEY environment variable not set[/bold red]")) sys.exit(1) try: # Run the agent and get response response = asyncio.run(run_custom_tool_agent(args.prompt)) # Display the response console.print(Panel(response, title="Financial Assistant Response", border_style="green")) except Exception as e: console.print(Panel(f"[bold red]Error: {str(e)}[/bold red]")) sys.exit(1) # Test functions def test_custom_tools(): """Test that the custom tools work correctly.""" # Test currency conversion currency_result = convert_currency(CurrencyConversionInput( amount=100, from_currency="USD", to_currency="EUR" )) assert "USD" in currency_result assert "EUR" in currency_result # Test stock price stock_result = get_stock_price(StockPriceInput(symbol="AAPL")) assert "AAPL" in stock_result assert "$" in stock_result def test_create_financial_assistant(): """Test that the financial assistant agent is created with the correct configuration.""" agent = create_financial_assistant() assert agent.name == "FinancialAssistant" assert "financial assistant" in agent.instructions.lower() assert len(agent.tools) == 2 assert any(tool.name == "convert_currency" for tool in agent.tools) assert any(tool.name == "get_stock_price" for tool in agent.tools) def test_run_custom_tool_agent(): """Test that the agent can use custom tools and produce a response.""" import pytest # Skip this test if no API key is available if not os.environ.get("OPENAI_API_KEY"): pytest.skip("OPENAI_API_KEY not set") # Run a test query that should use the currency conversion tool response = asyncio.run(run_custom_tool_agent("Convert 50 USD to EUR")) # Verify we got a non-empty response that mentions the currencies assert response assert len(response) > 0 assert "USD" in response assert "EUR" in response if __name__ == "__main__": main() ================================================ FILE: openai-agents-examples/07_agent_with_handoffs.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # "openai-agents>=0.0.2", # "pytest>=7.4.0", # "rich>=13.7.0", # ] # /// """ Agent with Handoffs Example This example demonstrates how to create agents that can hand off tasks to other specialized agents. It shows how to implement a customer support system with a triage agent and specialist agents. Run with: uv run 07_agent_with_handoffs.py --prompt "I need help with my billing" Test with: uv run pytest 07_agent_with_handoffs.py """ import os import sys import argparse import json import asyncio from typing import Optional, List, Dict, Any, Union from rich.console import Console from rich.panel import Panel from openai import OpenAI from agents import Agent, Runner, handoff # Initialize console for rich output console = Console() def create_billing_agent() -> Agent: """ Create a billing specialist agent. Returns: An Agent instance specialized in billing issues. """ instructions = """ You are a billing specialist who can help customers with billing-related issues. You can assist with questions about invoices, payment methods, refunds, and subscription plans. Be helpful, clear, and concise in your responses. Always verify the customer's information before providing specific account details. """ return Agent( name="BillingSpecialist", instructions=instructions, model="gpt-4o-mini", handoff_description="Use this agent for questions about billing, payments, invoices, or subscription issues." ) def create_technical_agent() -> Agent: """ Create a technical support agent. Returns: An Agent instance specialized in technical support. """ instructions = """ You are a technical support specialist who can help customers with technical issues. You can assist with questions about software functionality, bugs, error messages, and how-to guides. Provide clear step-by-step instructions when explaining technical procedures. Ask clarifying questions if the customer's issue is not clear. """ return Agent( name="TechnicalSupport", instructions=instructions, model="gpt-4o-mini", handoff_description="Use this agent for technical issues, bugs, error messages, or how-to questions." ) def create_account_agent() -> Agent: """ Create an account management agent. Returns: An Agent instance specialized in account management. """ instructions = """ You are an account management specialist who can help customers with account-related issues. You can assist with questions about account creation, profile updates, security settings, and account recovery. Always prioritize account security and verify the customer's identity before making changes. Provide clear guidance on how customers can manage their account settings. """ return Agent( name="AccountManager", instructions=instructions, model="gpt-4o-mini", handoff_description="Use this agent for account management, profile updates, or security questions." ) def create_triage_agent(specialists: List[Agent]) -> Agent: """ Create a triage agent that can delegate to specialist agents. Args: specialists: List of specialist agents to which tasks can be delegated Returns: An Agent instance that triages customer inquiries """ instructions = """ You are a customer support triage agent. Your job is to: 1. Understand the customer's issue 2. Determine which specialist would be best suited to help 3. Hand off the conversation to that specialist Be polite and professional. If you're unsure which specialist to choose, ask clarifying questions. """ # Create handoffs to specialist agents handoffs = [handoff(agent) for agent in specialists] return Agent( name="TriageAgent", instructions=instructions, model="gpt-4o-mini", handoffs=handoffs ) async def run_customer_support_system(prompt: str) -> str: """ Run the customer support system with the given prompt. Args: prompt: The customer's inquiry Returns: The final response from the appropriate specialist agent """ # Create specialist agents billing_agent = create_billing_agent() technical_agent = create_technical_agent() account_agent = create_account_agent() # Create triage agent with specialists triage_agent = create_triage_agent([billing_agent, technical_agent, account_agent]) # Run the triage agent with the prompt result = await Runner.run(triage_agent, prompt) # Return the final response return result.final_output def main(): """Main function to parse arguments and run the customer support system.""" parser = argparse.ArgumentParser(description="Agent with Handoffs Example") parser.add_argument("--prompt", "-p", type=str, required=True, help="The customer inquiry to send to the support system") args = parser.parse_args() # Ensure API key is available if not os.environ.get("OPENAI_API_KEY"): console.print(Panel("[bold red]Error: OPENAI_API_KEY environment variable not set[/bold red]")) sys.exit(1) try: # Run the customer support system and get response response = asyncio.run(run_customer_support_system(args.prompt)) # Display the response console.print(Panel(response, title="Customer Support Response", border_style="green")) except Exception as e: console.print(Panel(f"[bold red]Error: {str(e)}[/bold red]")) sys.exit(1) # Test functions def test_create_specialist_agents(): """Test that specialist agents are created with the correct configuration.""" billing_agent = create_billing_agent() technical_agent = create_technical_agent() account_agent = create_account_agent() assert billing_agent.name == "BillingSpecialist" assert technical_agent.name == "TechnicalSupport" assert account_agent.name == "AccountManager" assert "billing specialist" in billing_agent.instructions.lower() assert "technical support" in technical_agent.instructions.lower() assert "account management" in account_agent.instructions.lower() def test_create_triage_agent(): """Test that the triage agent is created with the correct configuration.""" billing_agent = create_billing_agent() technical_agent = create_technical_agent() account_agent = create_account_agent() triage_agent = create_triage_agent([billing_agent, technical_agent, account_agent]) assert triage_agent.name == "TriageAgent" assert "triage agent" in triage_agent.instructions.lower() assert len(triage_agent.handoffs) == 3 def test_run_customer_support_system(): """Test that the customer support system can run and produce a response.""" import pytest # Skip this test if no API key is available if not os.environ.get("OPENAI_API_KEY"): pytest.skip("OPENAI_API_KEY not set") # Run a test query that should go to the billing specialist response = asyncio.run(run_customer_support_system("I have a question about my recent invoice")) # Verify we got a non-empty response assert response assert len(response) > 0 if __name__ == "__main__": main() ================================================ FILE: openai-agents-examples/08_agent_with_agent_as_tool.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # "openai-agents>=0.0.2", # "pytest>=7.4.0", # "rich>=13.7.0", # ] # /// """ Agent with Agent as Tool Example This example demonstrates how to use an agent as a tool for another agent. It shows how to create a research agent that can be used as a tool by a blog writer agent. Run with: uv run 08_agent_with_agent_as_tool.py --prompt "Write a blog post about music theory" Test with: uv run pytest 08_agent_with_agent_as_tool.py """ import os import sys import argparse import json import asyncio from typing import Optional, List, Dict, Any, Union from rich.console import Console from rich.panel import Panel from openai import OpenAI from agents import Agent, Runner # Initialize console for rich output console = Console() def create_research_agent() -> Agent: """ Create a research agent that can gather information on topics. Returns: An Agent instance specialized in research. """ instructions = """ You are a research specialist who excels at gathering accurate information on various topics. Your responses should be factual, well-organized, and comprehensive. Include relevant details, statistics, and context when available. Always cite your sources if you're providing specific facts or quotes. Focus on providing high-quality, reliable information that would be useful for content creation. """ return Agent( name="ResearchSpecialist", instructions=instructions, model="gpt-4o-mini", ) def create_blog_writer_agent(research_agent: Agent) -> Agent: """ Create a blog writer agent that can use a research agent as a tool. Args: research_agent: The research agent to use as a tool Returns: An Agent instance specialized in blog writing with research capabilities """ instructions = """ You are a professional blog writer who creates engaging, informative content. Your writing should be clear, conversational, and tailored to a general audience. Structure your blog posts with an introduction, body paragraphs, and conclusion. Use the research tool available to you to gather accurate information on topics. Incorporate the research seamlessly into your writing while maintaining your voice. """ # Convert the research agent into a tool research_tool = research_agent.as_tool( tool_name="research_topic", tool_description="Research a specific topic to gather accurate information. Provide a clear, specific topic or question to research.", ) return Agent( name="BlogWriter", instructions=instructions, model="gpt-4o-mini", tools=[research_tool], ) async def run_blog_writer_system(prompt: str) -> str: """ Run the blog writer system with the given prompt. Args: prompt: The topic or request for a blog post Returns: The blog post content """ # Create the research agent research_agent = create_research_agent() # Create the blog writer agent with the research agent as a tool blog_writer = create_blog_writer_agent(research_agent) # Run the blog writer agent with the prompt result = await Runner.run(blog_writer, prompt) # Return the blog post return result.final_output def main(): """Main function to parse arguments and run the blog writer system.""" parser = argparse.ArgumentParser(description="Agent with Agent as Tool Example") parser.add_argument( "--prompt", "-p", type=str, required=True, help="The topic or request for a blog post", ) args = parser.parse_args() # Ensure API key is available if not os.environ.get("OPENAI_API_KEY"): console.print( Panel( "[bold red]Error: OPENAI_API_KEY environment variable not set[/bold red]" ) ) sys.exit(1) try: # Run the blog writer system and get the blog post blog_post = asyncio.run(run_blog_writer_system(args.prompt)) # Display the blog post console.print(Panel(blog_post, title="Blog Post", border_style="green")) except Exception as e: console.print(Panel(f"[bold red]Error: {str(e)}[/bold red]")) sys.exit(1) # Test functions def test_create_research_agent(): """Test that the research agent is created with the correct configuration.""" agent = create_research_agent() assert agent.name == "ResearchSpecialist" assert "research specialist" in agent.instructions.lower() assert agent.model == "gpt-4o-mini" def test_create_blog_writer_agent(): """Test that the blog writer agent is created with the correct configuration.""" research_agent = create_research_agent() blog_writer = create_blog_writer_agent(research_agent) assert blog_writer.name == "BlogWriter" assert "blog writer" in blog_writer.instructions.lower() assert len(blog_writer.tools) == 1 assert blog_writer.tools[0].name == "research_topic" def test_run_blog_writer_system(): """Test that the blog writer system can run and produce a blog post.""" import pytest # Skip this test if no API key is available if not os.environ.get("OPENAI_API_KEY"): pytest.skip("OPENAI_API_KEY not set") # Run a test query for a simple blog post blog_post = asyncio.run( run_blog_writer_system("Write a short blog post about artificial intelligence") ) # Verify we got a non-empty blog post assert blog_post assert len(blog_post) > 0 # The blog post should contain relevant terms assert any( term in blog_post.lower() for term in ["ai", "artificial intelligence", "technology"] ) if __name__ == "__main__": main() ================================================ FILE: openai-agents-examples/09_agent_with_context_management.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # "openai-agents>=0.0.2", # "pytest>=7.4.0", # "rich>=13.7.0", # ] # /// """ Agent with Context Management Example This example demonstrates how to use context management with agents to maintain state across multiple interactions. It shows how to create a conversation agent that remembers previous interactions. Run with: uv run 09_agent_with_context_management.py --prompt "Tell me about Mars" Test with: uv run pytest 09_agent_with_context_management.py """ import os import sys import argparse import json import asyncio from typing import Optional, List, Dict, Any, Union from rich.console import Console from rich.panel import Panel from openai import OpenAI from agents import Agent, Runner, Context # Initialize console for rich output console = Console() def create_conversation_agent() -> Agent: """ Create a conversation agent that can maintain context. Returns: An Agent instance that maintains conversation context. """ instructions = """ You are a helpful conversational assistant that maintains context across interactions. Remember details from previous parts of the conversation and refer back to them when relevant. Be friendly, informative, and engaging in your responses. If the user asks about something you discussed earlier, acknowledge that and build upon it. """ return Agent( name="ConversationAssistant", instructions=instructions, model="gpt-4o-mini", ) async def run_conversation_with_context(prompt: str, context: Optional[Context] = None) -> tuple[str, Context]: """ Run a conversation agent with context management. Args: prompt: The user's query or prompt context: Optional existing context from previous interactions Returns: A tuple containing the agent's response and the updated context """ # Create the conversation agent agent = create_conversation_agent() # Create a new context if none is provided if context is None: context = Context() # Run the agent with the prompt and context result = await Runner.run(agent, prompt, context=context) # Return the response and updated context return result.final_output, result.context def simulate_conversation(initial_prompt: str, follow_up_prompts: List[str]) -> List[str]: """ Simulate a multi-turn conversation with context management. Args: initial_prompt: The first user prompt follow_up_prompts: List of follow-up prompts Returns: List of agent responses """ responses = [] context = None # Run the initial prompt response, context = asyncio.run(run_conversation_with_context(initial_prompt, context)) responses.append(result.final_output) # Run each follow-up prompt with the updated context for prompt in follow_up_prompts: response, context = asyncio.run(run_conversation_with_context(prompt, context)) responses.append(result.final_output) return responses def main(): """Main function to parse arguments and run the conversation agent.""" parser = argparse.ArgumentParser(description="Agent with Context Management Example") parser.add_argument("--prompt", "-p", type=str, required=True, help="The prompt to send to the agent") parser.add_argument("--follow-up", "-f", type=str, nargs="*", default=[], help="Optional follow-up prompts to simulate a conversation") args = parser.parse_args() # Ensure API key is available if not os.environ.get("OPENAI_API_KEY"): console.print(Panel("[bold red]Error: OPENAI_API_KEY environment variable not set[/bold red]")) sys.exit(1) try: # Simulate a conversation with the provided prompts responses = simulate_conversation(args.prompt, args.follow_up) # Display the initial response console.print(Panel(responses[0], title=f"Response to: {args.prompt}", border_style="green")) # Display follow-up responses if any for i, response in enumerate(responses[1:]): console.print(Panel(response, title=f"Response to: {args.follow_up[i]}", border_style="blue")) except Exception as e: console.print(Panel(f"[bold red]Error: {str(e)}[/bold red]")) sys.exit(1) # Test functions def test_create_conversation_agent(): """Test that the conversation agent is created with the correct configuration.""" agent = create_conversation_agent() assert agent.name == "ConversationAssistant" assert "conversational assistant" in agent.instructions.lower() assert agent.model == "gpt-4o-mini" def test_run_conversation_with_context(): """Test that the agent can maintain context across interactions.""" import pytest # Skip this test if no API key is available if not os.environ.get("OPENAI_API_KEY"): pytest.skip("OPENAI_API_KEY not set") # Run an initial query initial_prompt = "Tell me about Mars" response, context = asyncio.run(run_conversation_with_context(initial_prompt)) # Verify we got a non-empty response assert response assert len(response) > 0 assert context is not None # Run a follow-up query that references the previous conversation follow_up_prompt = "How long would it take to travel there?" follow_up_response, _ = asyncio.run(run_conversation_with_context(follow_up_prompt, context)) # Verify the follow-up response acknowledges the previous context assert follow_up_response assert len(follow_up_response) > 0 # The response should contain terms related to Mars travel assert any(term in follow_up_response.lower() for term in ["mars", "travel", "journey", "months"]) if __name__ == "__main__": main() ================================================ FILE: openai-agents-examples/10_agent_with_guardrails.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # "openai-agents>=0.0.2", # "pytest>=7.4.0", # "rich>=13.7.0", # "pydantic>=2.0.0", # ] # /// """ Agent with Guardrails Example This example demonstrates how to use guardrails with agents to filter and validate inputs. It shows how to create an agent with input validation to prevent prompt injection and ensure proper input format. Run with: uv run 10_agent_with_guardrails.py --prompt "Summarize this article about renewable energy" Test with: uv run pytest 10_agent_with_guardrails.py """ import os import sys import argparse import json import asyncio import re from typing import Optional, List, Dict, Any, Union from rich.console import Console from rich.panel import Panel from pydantic import BaseModel, Field from openai import OpenAI from agents import Agent, Runner, InputGuardrail # Initialize console for rich output console = Console() # Define a custom input guardrail for content moderation class ContentModerationGuardrail(InputGuardrail): """ A guardrail that filters out potentially harmful or inappropriate content. """ def __init__(self): """Initialize the content moderation guardrail.""" # List of terms to filter out (simplified for example purposes) self.filtered_terms = [ "hack", "exploit", "bypass", "illegal", "steal", "attack", "malware", "virus", "phishing", "scam", "fraud" ] def filter(self, input_str: str) -> Optional[str]: """ Filter the input string for potentially harmful content. Args: input_str: The input string to filter Returns: The filtered string if it passes, or None if it should be rejected """ # Convert to lowercase for case-insensitive matching lower_input = input_str.lower() # Check for filtered terms for term in self.filtered_terms: if term in lower_input: return None # Reject the input return input_str # Accept the input def get_rejection_message(self, input_str: str) -> str: """ Get a message explaining why the input was rejected. Args: input_str: The rejected input string Returns: A message explaining the rejection """ return "Your input contains terms that may be related to harmful or inappropriate content. Please rephrase your request." # Define a custom input guardrail for input format validation class FormatValidationGuardrail(InputGuardrail): """ A guardrail that ensures inputs follow a specific format. """ def __init__(self, min_length: int = 5, max_length: int = 500): """ Initialize the format validation guardrail. Args: min_length: Minimum allowed input length max_length: Maximum allowed input length """ self.min_length = min_length self.max_length = max_length def filter(self, input_str: str) -> Optional[str]: """ Filter the input string based on format requirements. Args: input_str: The input string to filter Returns: The input string if it passes, or None if it should be rejected """ # Check length constraints if len(input_str) < self.min_length: return None # Too short if len(input_str) > self.max_length: return None # Too long return input_str # Accept the input def get_rejection_message(self, input_str: str) -> str: """ Get a message explaining why the input was rejected. Args: input_str: The rejected input string Returns: A message explaining the rejection """ if len(input_str) < self.min_length: return f"Your input is too short. Please provide at least {self.min_length} characters." if len(input_str) > self.max_length: return f"Your input is too long. Please limit your request to {self.max_length} characters." return "Your input does not meet the format requirements." def create_protected_agent() -> Agent: """ Create an agent with input guardrails for protection. Returns: An Agent instance with input guardrails. """ instructions = """ You are a helpful assistant that provides information and assistance on various topics. You prioritize user safety and ethical responses. Provide accurate, helpful information while avoiding potentially harmful content. Be concise but thorough in your responses. """ # Create guardrails content_guardrail = ContentModerationGuardrail() format_guardrail = FormatValidationGuardrail(min_length=5, max_length=500) # Create the agent with guardrails return Agent( name="ProtectedAssistant", instructions=instructions, model="gpt-4o-mini", input_guardrails=[content_guardrail, format_guardrail] ) async def run_protected_agent(prompt: str) -> str: """ Run the protected agent with the given prompt. Args: prompt: The user's query or prompt Returns: The agent's response as a string, or a rejection message if the input is filtered """ # Create the protected agent agent = create_protected_agent() try: # Run the agent with the prompt result = await Runner.run(agent, prompt) return result.final_output except Exception as e: # Check if it's a guardrail rejection if "guardrail rejected" in str(e).lower(): return f"Input rejected by guardrails: {str(e)}" # Other exception return f"Error: {str(e)}" def main(): """Main function to parse arguments and run the protected agent.""" parser = argparse.ArgumentParser(description="Agent with Guardrails Example") parser.add_argument("--prompt", "-p", type=str, required=True, help="The prompt to send to the agent") args = parser.parse_args() # Ensure API key is available if not os.environ.get("OPENAI_API_KEY"): console.print(Panel("[bold red]Error: OPENAI_API_KEY environment variable not set[/bold red]")) sys.exit(1) try: # Run the protected agent and get response response = asyncio.run(run_protected_agent(args.prompt)) # Display the response if "rejected" in response.lower(): console.print(Panel(response, title="Input Rejected", border_style="red")) else: console.print(Panel(response, title="Protected Agent Response", border_style="green")) except Exception as e: console.print(Panel(f"[bold red]Error: {str(e)}[/bold red]")) sys.exit(1) # Test functions def test_content_moderation_guardrail(): """Test that the content moderation guardrail correctly filters inputs.""" guardrail = ContentModerationGuardrail() # Test safe input safe_input = "Tell me about renewable energy sources" assert guardrail.filter(safe_input) == safe_input # Test unsafe input unsafe_input = "How to hack into a computer system" assert guardrail.filter(unsafe_input) is None assert "harmful" in guardrail.get_rejection_message(unsafe_input) def test_format_validation_guardrail(): """Test that the format validation guardrail correctly validates inputs.""" guardrail = FormatValidationGuardrail(min_length=5, max_length=20) # Test valid input valid_input = "Hello world" assert guardrail.filter(valid_input) == valid_input # Test too short input short_input = "Hi" assert guardrail.filter(short_input) is None assert "short" in guardrail.get_rejection_message(short_input) # Test too long input long_input = "This is a very long input that exceeds the maximum allowed length" assert guardrail.filter(long_input) is None assert "long" in guardrail.get_rejection_message(long_input) def test_create_protected_agent(): """Test that the protected agent is created with the correct configuration.""" agent = create_protected_agent() assert agent.name == "ProtectedAssistant" assert "helpful assistant" in agent.instructions.lower() assert agent.model == "gpt-4o-mini" assert len(agent.input_guardrails) == 2 def test_run_protected_agent(): """Test that the protected agent can run and produce a response or rejection.""" import pytest # Skip this test if no API key is available if not os.environ.get("OPENAI_API_KEY"): pytest.skip("OPENAI_API_KEY not set") # Test with a valid prompt valid_prompt = "Tell me about renewable energy sources" valid_response = asyncio.run(run_protected_agent(valid_prompt)) # Verify we got a non-empty response assert valid_response assert len(valid_response) > 0 assert "rejected" not in valid_response.lower() # Test with an invalid prompt (contains filtered term) invalid_prompt = "How to hack into a system" invalid_response = asyncio.run(run_protected_agent(invalid_prompt)) # Verify we got a rejection message assert invalid_response assert "rejected" in invalid_response.lower() if __name__ == "__main__": main() ================================================ FILE: openai-agents-examples/11_agent_orchestration.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # "openai-agents>=0.0.2", # "pytest>=7.4.0", # "rich>=13.7.0", # ] # /// """ Agent Orchestration Example This example demonstrates how to orchestrate multiple agents to work together on complex tasks. It shows how to create a system where specialized agents collaborate under the coordination of a manager agent. Run with: uv run 11_agent_orchestration.py --prompt "Create a blog post about climate change solutions" Test with: uv run pytest 11_agent_orchestration.py """ import os import sys import argparse import json import asyncio from typing import Optional, List, Dict, Any, Union, Tuple from rich.console import Console from rich.panel import Panel from openai import OpenAI from agents import Agent, Runner, handoff, Context # Initialize console for rich output console = Console() def create_research_agent() -> Agent: """ Create a research agent that gathers information. Returns: An Agent instance specialized in research. """ instructions = """ You are a research specialist who excels at gathering accurate information on various topics. Your task is to collect relevant facts, statistics, and context on the assigned topic. Focus on providing comprehensive, well-organized information that covers different aspects of the topic. Include both general information and specific details that would be useful for content creation. Always prioritize accuracy and cite sources when providing specific facts. """ return Agent( name="ResearchSpecialist", instructions=instructions, model="gpt-4o-mini", handoff_description="Use this agent to gather comprehensive information on a topic." ) def create_outline_agent() -> Agent: """ Create an outline agent that structures content. Returns: An Agent instance specialized in creating outlines. """ instructions = """ You are an outline specialist who excels at organizing information into clear, logical structures. Your task is to create well-structured outlines for content based on research provided. Include main sections, subsections, and key points to cover in each section. Ensure the outline has a logical flow and covers the topic comprehensively. Focus on creating a structure that will engage readers while effectively communicating information. """ return Agent( name="OutlineSpecialist", instructions=instructions, model="gpt-4o-mini", handoff_description="Use this agent to create a structured outline based on research." ) def create_content_agent() -> Agent: """ Create a content agent that writes engaging content. Returns: An Agent instance specialized in content writing. """ instructions = """ You are a content writing specialist who excels at creating engaging, informative content. Your task is to write high-quality content based on the provided outline and research. Use a conversational, engaging tone while maintaining accuracy and clarity. Include an attention-grabbing introduction, well-developed body paragraphs, and a compelling conclusion. Incorporate the research seamlessly into the content while maintaining a consistent voice. """ return Agent( name="ContentSpecialist", instructions=instructions, model="gpt-4o-mini", handoff_description="Use this agent to write engaging content based on an outline and research." ) def create_editor_agent() -> Agent: """ Create an editor agent that refines and polishes content. Returns: An Agent instance specialized in editing. """ instructions = """ You are an editing specialist who excels at refining and polishing content. Your task is to review content for clarity, coherence, grammar, and style. Improve sentence structure, word choice, and flow while maintaining the original voice. Ensure the content is well-organized, engaging, and free of errors. Focus on making the content more impactful and reader-friendly. """ return Agent( name="EditingSpecialist", instructions=instructions, model="gpt-4o-mini", handoff_description="Use this agent to refine and polish content." ) def create_manager_agent(specialists: List[Agent]) -> Agent: """ Create a manager agent that coordinates the work of specialist agents. Args: specialists: List of specialist agents to coordinate Returns: An Agent instance that manages the content creation process """ instructions = """ You are a content manager who coordinates the work of specialist agents to create high-quality content. Your task is to: 1. Understand the content request 2. Delegate research to the Research Specialist 3. Have the Outline Specialist create a structure based on the research 4. Have the Content Specialist write content based on the outline and research 5. Have the Editing Specialist refine and polish the content 6. Deliver the final polished content Manage the workflow efficiently and ensure each specialist has the information they need. """ # Create handoffs to specialist agents handoffs = [handoff(agent) for agent in specialists] return Agent( name="ContentManager", instructions=instructions, model="gpt-4o-mini", handoffs=handoffs ) async def orchestrate_content_creation(prompt: str) -> str: """ Orchestrate the content creation process with multiple specialized agents. Args: prompt: The content request Returns: The final polished content """ # Create specialist agents research_agent = create_research_agent() outline_agent = create_outline_agent() content_agent = create_content_agent() editor_agent = create_editor_agent() # Create manager agent with specialists manager = create_manager_agent([research_agent, outline_agent, content_agent, editor_agent]) # Create a context to track the workflow context = Context() # Run the manager agent with the prompt and context result = await Runner.run(manager, prompt, context=context) # Return the final content return result.final_output def main(): """Main function to parse arguments and run the content creation system.""" parser = argparse.ArgumentParser(description="Agent Orchestration Example") parser.add_argument("--prompt", "-p", type=str, required=True, help="The content request to process") args = parser.parse_args() # Ensure API key is available if not os.environ.get("OPENAI_API_KEY"): console.print(Panel("[bold red]Error: OPENAI_API_KEY environment variable not set[/bold red]")) sys.exit(1) try: # Run the content creation system and get the final content console.print(Panel("Starting content creation process...", title="Status", border_style="blue")) content = asyncio.run(orchestrate_content_creation(args.prompt)) # Display the final content console.print(Panel(content, title="Final Content", border_style="green")) except Exception as e: console.print(Panel(f"[bold red]Error: {str(e)}[/bold red]")) sys.exit(1) # Test functions def test_create_specialist_agents(): """Test that specialist agents are created with the correct configuration.""" research_agent = create_research_agent() outline_agent = create_outline_agent() content_agent = create_content_agent() editor_agent = create_editor_agent() assert research_agent.name == "ResearchSpecialist" assert outline_agent.name == "OutlineSpecialist" assert content_agent.name == "ContentSpecialist" assert editor_agent.name == "EditingSpecialist" assert "research specialist" in research_agent.instructions.lower() assert "outline specialist" in outline_agent.instructions.lower() assert "content writing specialist" in content_agent.instructions.lower() assert "editing specialist" in editor_agent.instructions.lower() def test_create_manager_agent(): """Test that the manager agent is created with the correct configuration.""" research_agent = create_research_agent() outline_agent = create_outline_agent() content_agent = create_content_agent() editor_agent = create_editor_agent() manager = create_manager_agent([research_agent, outline_agent, content_agent, editor_agent]) assert manager.name == "ContentManager" assert "content manager" in manager.instructions.lower() assert len(manager.handoffs) == 4 def test_orchestrate_content_creation(): """Test that the content creation system can run and produce content.""" import pytest # Skip this test if no API key is available if not os.environ.get("OPENAI_API_KEY"): pytest.skip("OPENAI_API_KEY not set") # Run a test with a simple content request # Use a shorter timeout for testing content = asyncio.run(orchestrate_content_creation("Write a short paragraph about renewable energy")) # Verify we got non-empty content assert content assert len(content) > 0 # The content should contain relevant terms assert any(term in content.lower() for term in ["renewable", "energy", "sustainable"]) if __name__ == "__main__": main() ================================================ FILE: openai-agents-examples/12_anthropic_agent.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # "openai-agents>=0.0.2", # "anthropic>=0.45.2", # "pytest>=7.4.0", # "rich>=13.7.0", # ] # /// """ Anthropic Agent Example This example demonstrates how to use the OpenAI Agents SDK with Anthropic's Claude model. It shows how to create a custom model provider that works with Anthropic's API. Run with: uv run 12_anthropic_agent.py --prompt "Explain the concept of quantum entanglement" Test with: uv run pytest 12_anthropic_agent.py """ import os import sys import argparse import json import asyncio from typing import Optional, List, Dict, Any, Union, Callable from rich.console import Console from rich.panel import Panel import anthropic from openai import OpenAI from agents import Agent, Runner from openai_agents.providers import ModelProvider, ModelResponse from openai.types.chat import ChatCompletion, ChatCompletionMessage # Initialize console for rich output console = Console() class AnthropicModelProvider(ModelProvider): """ Custom model provider for Anthropic's Claude model. """ def __init__(self, api_key: Optional[str] = None): """ Initialize the Anthropic model provider. Args: api_key: Anthropic API key. If None, will use the ANTHROPIC_API_KEY environment variable. """ self.api_key = api_key or os.environ.get("ANTHROPIC_API_KEY") if not self.api_key: raise ValueError("Anthropic API key is required") self.client = anthropic.Anthropic(api_key=self.api_key) async def generate( self, messages: List[Dict[str, Any]], model: str, temperature: float = 0.7, max_tokens: int = 1024, **kwargs ) -> ModelResponse: """ Generate a response using Anthropic's Claude model. Args: messages: List of messages in the conversation model: Model name (will be mapped to Anthropic model) temperature: Sampling temperature max_tokens: Maximum number of tokens to generate **kwargs: Additional arguments to pass to the model Returns: A ModelResponse containing the model's response """ # Map OpenAI model names to Anthropic model names model_mapping = { "gpt-4o-mini": "claude-3-haiku-20240307", "gpt-4o": "claude-3-opus-20240229", "gpt-3.5-turbo": "claude-3-sonnet-20240229", } # Use the mapped model or default to claude-3-haiku anthropic_model = model_mapping.get(model, "claude-3-haiku-20240307") # Convert OpenAI message format to Anthropic message format anthropic_messages = [] for message in messages: role = message["role"] # Map OpenAI roles to Anthropic roles if role == "system": # System messages are handled differently in Anthropic system_content = message.get("content", "") continue elif role == "user": anthropic_role = "user" elif role == "assistant": anthropic_role = "assistant" else: # Skip unsupported roles continue # Add the message anthropic_messages.append({ "role": anthropic_role, "content": message.get("content", "") }) # Create the message with system prompt if available try: response = await self.client.messages.create( model=anthropic_model, messages=anthropic_messages, system=system_content if 'system_content' in locals() else "", temperature=temperature, max_tokens=max_tokens, **kwargs ) # Convert Anthropic response to OpenAI format output_message = { "role": "assistant", "content": response.content[0].text } # Create a ModelResponse return ModelResponse( output=[output_message], usage={ "prompt_tokens": response.usage.input_tokens, "completion_tokens": response.usage.output_tokens, "total_tokens": response.usage.input_tokens + response.usage.output_tokens }, referenceable_id=None ) except Exception as e: raise Exception(f"Error generating response from Anthropic: {str(e)}") def create_anthropic_agent() -> Agent: """ Create an agent that uses Anthropic's Claude model. Returns: An Agent instance that uses Anthropic's Claude model. """ instructions = """ You are a helpful assistant powered by Anthropic's Claude model. You provide accurate, thoughtful responses to user queries. You excel at explaining complex concepts in clear, accessible language. When appropriate, you break down information into easy-to-understand parts. You acknowledge when you don't know something rather than making up information. """ # Create the Anthropic model provider provider = AnthropicModelProvider() # Create the agent with the Anthropic provider return Agent( name="ClaudeAssistant", instructions=instructions, model="gpt-4o-mini", # This will be mapped to claude-3-haiku model_provider=provider ) async def run_anthropic_agent(prompt: str) -> str: """ Run the Anthropic agent with the given prompt. Args: prompt: The user's query or prompt Returns: The agent's response as a string """ # Create the Anthropic agent agent = create_anthropic_agent() # Run the agent with the prompt result = await Runner.run(agent, prompt) # Return the response return result.final_output def main(): """Main function to parse arguments and run the Anthropic agent.""" parser = argparse.ArgumentParser(description="Anthropic Agent Example") parser.add_argument("--prompt", "-p", type=str, required=True, help="The prompt to send to the agent") args = parser.parse_args() # Ensure API key is available if not os.environ.get("ANTHROPIC_API_KEY"): console.print(Panel("[bold red]Error: ANTHROPIC_API_KEY environment variable not set[/bold red]")) sys.exit(1) try: # Run the Anthropic agent and get response response = asyncio.run(run_anthropic_agent(args.prompt)) # Display the response console.print(Panel(response, title="Claude Response", border_style="green")) except Exception as e: console.print(Panel(f"[bold red]Error: {str(e)}[/bold red]")) sys.exit(1) # Test functions def test_create_anthropic_agent(): """Test that the Anthropic agent is created with the correct configuration.""" import pytest # Skip this test if no API key is available if not os.environ.get("ANTHROPIC_API_KEY"): pytest.skip("ANTHROPIC_API_KEY not set") agent = create_anthropic_agent() assert agent.name == "ClaudeAssistant" assert "claude" in agent.instructions.lower() assert isinstance(agent.model_provider, AnthropicModelProvider) def test_run_anthropic_agent(): """Test that the Anthropic agent can run and produce a response.""" import pytest # Skip this test if no API key is available if not os.environ.get("ANTHROPIC_API_KEY"): pytest.skip("ANTHROPIC_API_KEY not set") # Run a simple test query response = asyncio.run(run_anthropic_agent("What is 2+2?")) # Verify we got a non-empty response assert response assert len(response) > 0 # The response should contain "4" somewhere assert "4" in response if __name__ == "__main__": main() ================================================ FILE: openai-agents-examples/13_research_blog_system.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # "openai-agents>=0.0.2", # "pytest>=7.4.0", # "rich>=13.7.0", # "markdown>=3.5.2", # ] # /// """ Research and Blog Agent System This example demonstrates a complete system where research agents and blog agents work together to create markdown blogs. It showcases the integration of multiple agent capabilities. Run with: uv run 13_research_blog_system.py --topic "Artificial Intelligence Ethics" --output blog_post.md Test with: uv run pytest 13_research_blog_system.py """ import os import sys import argparse import json import asyncio import markdown from datetime import datetime from typing import Optional, List, Dict, Any, Union, Tuple from rich.console import Console from rich.panel import Panel from rich.markdown import Markdown from openai import OpenAI from agents import Agent, Runner, handoff, Context, function_tool # Initialize console for rich output console = Console() # Define function tools for the research agent @function_tool def search_for_information(query: str, depth: int = 3) -> str: """ Simulated search for information on a topic. Args: query: The search query depth: The depth of the search (1-5, with 5 being most comprehensive) Returns: A string containing the search results """ # This is a mock implementation - in a real application, you would call a search API search_results = { "artificial intelligence ethics": """ Artificial Intelligence Ethics is a field focused on ensuring AI systems are developed and used responsibly. Key principles include: 1. Transparency - AI systems should be explainable and understandable 2. Fairness - AI should not perpetuate or amplify biases 3. Privacy - AI systems should respect user privacy and data rights 4. Accountability - Clear responsibility for AI decisions and impacts 5. Safety - AI systems should be reliable and minimize harm Current challenges include: - Algorithmic bias in facial recognition and criminal justice systems - Privacy concerns with data collection and surveillance - Autonomous weapons and military applications - Job displacement due to automation - Concentration of AI power among few tech companies Organizations like the IEEE, EU Commission, and various academic institutions have developed ethical frameworks for AI development. """, "climate change solutions": """ Climate Change Solutions encompass various approaches to mitigate and adapt to global warming. Key mitigation strategies include: 1. Renewable energy transition (solar, wind, hydro, geothermal) 2. Energy efficiency improvements in buildings and industry 3. Sustainable transportation (electric vehicles, public transit) 4. Carbon capture and storage technologies 5. Reforestation and ecosystem restoration Adaptation strategies include: - Climate-resilient infrastructure - Water conservation and management - Sustainable agriculture practices - Early warning systems for extreme weather - Planned relocation of vulnerable communities Policy approaches include carbon pricing, regulations, subsidies for clean technology, and international agreements like the Paris Climate Accord. Emerging technologies such as green hydrogen, advanced batteries, and direct air capture show promise for addressing climate challenges. """, "quantum computing": """ Quantum Computing leverages quantum mechanics principles to process information in fundamentally new ways. Key concepts: 1. Qubits - Unlike classical bits (0 or 1), qubits can exist in superposition of states 2. Entanglement - Quantum particles become correlated, enabling complex computations 3. Quantum gates - Operations that manipulate qubits to perform calculations Potential applications: - Cryptography and security (both breaking existing systems and creating new ones) - Drug discovery and materials science through molecular simulation - Optimization problems in logistics, finance, and energy - Machine learning and AI acceleration - Climate modeling and complex system simulation Current state: Quantum computers remain in early development with 50-100+ qubit systems from IBM, Google, and others. Quantum advantage (surpassing classical computers) has been demonstrated for specific problems. Challenges include error correction, qubit stability (decoherence), and scaling systems to practical sizes. Major players include IBM, Google, Microsoft, IonQ, Rigetti, and various academic research groups. """ } # Find the most relevant result based on the query for key, value in search_results.items(): if any(word in query.lower() for word in key.split()): # Adjust the depth of information lines = value.strip().split('\n') result_depth = max(5, min(len(lines), depth * 5)) return '\n'.join(lines[:result_depth]) # Default response if no match found return "No specific information found on this topic. Please try a more general query." @function_tool def analyze_topic(topic: str) -> str: """ Analyze a topic to identify key aspects for research. Args: topic: The topic to analyze Returns: A string containing analysis of the topic with key aspects to research """ # This is a mock implementation - in a real application, this would be more sophisticated topic_analyses = { "artificial intelligence ethics": """ Topic Analysis: Artificial Intelligence Ethics Key aspects to research: 1. Ethical frameworks and principles for AI development 2. Bias and fairness in AI systems 3. Privacy implications of AI technologies 4. Accountability and transparency in AI decision-making 5. Regulatory approaches and governance models 6. Economic and social impacts of AI deployment 7. Case studies of ethical failures and successes 8. Future challenges and emerging ethical concerns """, "climate change solutions": """ Topic Analysis: Climate Change Solutions Key aspects to research: 1. Renewable energy technologies and implementation 2. Carbon capture and sequestration approaches 3. Policy mechanisms (carbon pricing, regulations, incentives) 4. Adaptation strategies for vulnerable communities 5. Agricultural and land use changes 6. Behavioral and lifestyle modifications 7. Economic considerations and just transition 8. International cooperation frameworks """, "quantum computing": """ Topic Analysis: Quantum Computing Key aspects to research: 1. Fundamental quantum mechanics principles relevant to computing 2. Quantum computing architectures and hardware approaches 3. Quantum algorithms and computational advantages 4. Potential applications across industries 5. Current state of development and key players 6. Challenges and limitations of quantum systems 7. Quantum programming languages and software tools 8. Timeline and roadmap for practical quantum computing """ } # Find the most relevant analysis for key, value in topic_analyses.items(): if any(word in topic.lower() for word in key.split()): return value.strip() # Default analysis if no match found return f""" Topic Analysis: {topic} Key aspects to research: 1. Historical context and development 2. Current state and major concepts 3. Key stakeholders and perspectives 4. Challenges and controversies 5. Future trends and developments 6. Practical applications and implications 7. Related fields and intersections 8. Resources for further learning """.strip() # Define function tools for the blog agent @function_tool def generate_blog_outline(topic: str, research: str) -> str: """ Generate an outline for a blog post based on research. Args: topic: The blog topic research: The research information to incorporate Returns: A string containing a structured blog outline """ # This is a simplified implementation - in a real application, this would use more sophisticated logic # Extract key points from research research_lines = research.strip().split('\n') key_points = [line.strip() for line in research_lines if line.strip() and not line.strip().startswith('#')] # Create a basic outline structure outline = f""" # Blog Outline: {topic} ## Introduction - Hook: Engaging opening to capture reader interest - Context: Brief background on {topic} - Thesis: Main point or argument of the blog post ## Main Section 1: Overview and Background - Historical context - Current relevance - Key concepts and definitions ## Main Section 2: Key Aspects and Analysis """ # Add research points to the outline for i, point in enumerate(key_points[:5]): if len(point) > 100: # Only use shorter points continue outline += f"\n - Point {i+1}: {point}" # Complete the outline outline += f""" ## Main Section 3: Implications and Applications - Practical applications - Future developments - Challenges and opportunities ## Conclusion - Summary of key points - Final thoughts - Call to action or next steps """ return outline.strip() @function_tool def format_blog_as_markdown(title: str, content: str) -> str: """ Format a blog post as markdown. Args: title: The blog post title content: The blog post content Returns: A string containing the formatted markdown """ # Ensure the content has proper markdown formatting if not content.startswith('#'): content = f"# {title}\n\n{content}" # Add metadata markdown = f"""--- title: "{title}" date: "{datetime.now().strftime('%Y-%m-%d')}" author: "AI Research & Blog System" tags: ["ai", "research", "blog"] --- {content} """ return markdown def create_research_agent() -> Agent: """ Create a research agent that gathers and analyzes information. Returns: An Agent instance specialized in research. """ instructions = """ You are a research specialist who excels at gathering and analyzing information on various topics. Your task is to: 1. Understand the research request 2. Use the search_for_information tool to gather relevant information 3. Use the analyze_topic tool to identify key aspects for research 4. Synthesize the information into a comprehensive, well-organized research report 5. Include relevant facts, statistics, and context 6. Ensure the research is accurate, balanced, and thorough Your research should be detailed enough to serve as the foundation for content creation. """ # Create the research agent with function tools return Agent( name="ResearchSpecialist", instructions=instructions, model="gpt-4o-mini", tools=[search_for_information, analyze_topic], handoff_description="Use this agent to conduct thorough research on a topic." ) def create_blog_agent() -> Agent: """ Create a blog agent that writes engaging blog posts. Returns: An Agent instance specialized in blog writing. """ instructions = """ You are a blog writing specialist who excels at creating engaging, informative blog posts. Your task is to: 1. Understand the blog request and research provided 2. Use the generate_blog_outline tool to create a structured outline 3. Write a comprehensive blog post based on the outline and research 4. Use the format_blog_as_markdown tool to format the post properly 5. Ensure the blog is engaging, informative, and well-structured Your blog posts should be conversational yet informative, with a clear introduction, well-developed body sections, and a compelling conclusion. """ # Create the blog agent with function tools return Agent( name="BlogSpecialist", instructions=instructions, model="gpt-4o-mini", tools=[generate_blog_outline, format_blog_as_markdown], handoff_description="Use this agent to write engaging blog posts based on research." ) def create_coordinator_agent(specialists: List[Agent]) -> Agent: """ Create a coordinator agent that manages the research and blog writing process. Args: specialists: List of specialist agents to coordinate Returns: An Agent instance that coordinates the content creation process """ instructions = """ You are a content coordinator who manages the process of creating research-based blog posts. Your task is to: 1. Understand the blog topic request 2. Delegate research to the Research Specialist 3. Provide the research to the Blog Specialist to create a blog post 4. Ensure the final blog post is comprehensive, engaging, and based on solid research 5. Deliver the final markdown blog post Manage the workflow efficiently and ensure each specialist has the information they need. """ # Create handoffs to specialist agents handoffs = [handoff(agent) for agent in specialists] return Agent( name="ContentCoordinator", instructions=instructions, model="gpt-4o-mini", handoffs=handoffs ) async def create_research_blog(topic: str) -> str: """ Create a research-based blog post on the given topic. Args: topic: The blog topic Returns: A string containing the markdown blog post """ # Create specialist agents research_agent = create_research_agent() blog_agent = create_blog_agent() # Create coordinator agent with specialists coordinator = create_coordinator_agent([research_agent, blog_agent]) # Create a context to track the workflow context = Context() # Run the coordinator agent with the topic and context result = await Runner.run(coordinator, f"Create a blog post about {topic}", context=context) # Return the final blog post return result.final_output def main(): """Main function to parse arguments and run the research blog system.""" parser = argparse.ArgumentParser(description="Research and Blog Agent System") parser.add_argument("--topic", "-t", type=str, required=True, help="The topic for the blog post") parser.add_argument("--output", "-o", type=str, default=None, help="Optional file path to save the markdown blog post") args = parser.parse_args() # Ensure API key is available if not os.environ.get("OPENAI_API_KEY"): console.print(Panel("[bold red]Error: OPENAI_API_KEY environment variable not set[/bold red]")) sys.exit(1) try: # Create the blog post console.print(Panel(f"Creating a blog post about '{args.topic}'...", title="Status", border_style="blue")) blog_post = asyncio.run(create_research_blog(args.topic)) # Display the blog post console.print(Panel(Markdown(blog_post), title="Blog Post", border_style="green")) # Save to file if output path is provided if args.output: with open(args.output, "w") as f: f.write(blog_post) console.print(f"[green]Blog post saved to {args.output}[/green]") except Exception as e: console.print(Panel(f"[bold red]Error: {str(e)}[/bold red]")) sys.exit(1) # Test functions def test_research_tools(): """Test that the research tools work correctly.""" # Test search tool search_result = search_for_information("artificial intelligence ethics") assert "ethics" in search_result.lower() assert "principles" in search_result.lower() # Test analysis tool analysis_result = analyze_topic("artificial intelligence ethics") assert "analysis" in analysis_result.lower() assert "key aspects" in analysis_result.lower() def test_blog_tools(): """Test that the blog tools work correctly.""" # Test outline tool outline = generate_blog_outline( "AI Ethics", "AI Ethics involves principles like transparency, fairness, and accountability." ) assert "introduction" in outline.lower() assert "conclusion" in outline.lower() # Test markdown formatting tool markdown = format_blog_as_markdown( "AI Ethics", "# AI Ethics\n\nThis is a blog post about AI ethics." ) assert "title" in markdown.lower() assert "date" in markdown.lower() assert "ai ethics" in markdown.lower() def test_create_agents(): """Test that the agents are created with the correct configuration.""" research_agent = create_research_agent() blog_agent = create_blog_agent() coordinator = create_coordinator_agent([research_agent, blog_agent]) assert research_agent.name == "ResearchSpecialist" assert blog_agent.name == "BlogSpecialist" assert coordinator.name == "ContentCoordinator" assert len(research_agent.tools) == 2 assert len(blog_agent.tools) == 2 assert len(coordinator.handoffs) == 2 def test_create_research_blog(): """Test that the research blog system can run and produce a blog post.""" import pytest # Skip this test if no API key is available if not os.environ.get("OPENAI_API_KEY"): pytest.skip("OPENAI_API_KEY not set") # Run a test with a simple topic # Use a shorter timeout for testing blog_post = asyncio.run(create_research_blog("AI Ethics")) # Verify we got a non-empty blog post assert blog_post assert len(blog_post) > 0 # The blog post should contain relevant terms assert any(term in blog_post.lower() for term in ["ai", "ethics", "principles"]) if __name__ == "__main__": main() ================================================ FILE: openai-agents-examples/README.md ================================================ # OpenAI Agents SDK Examples A comprehensive collection of single-file examples showcasing the capabilities of the OpenAI Agents SDK. ## Overview This repository contains 13 examples demonstrating various features of the OpenAI Agents SDK, with a focus on research and blog agents working together to create markdown blogs. Each example is implemented as a self-contained UV Python script with built-in tests. ## Key Features Demonstrated - Basic and multi-agent systems - Synchronous and asynchronous execution - Tracing and monitoring - Function tools and custom tools - Agent handoffs and agent-as-tool patterns - Context management - Guardrails for safety - Agent orchestration - Cross-provider integration (Anthropic) - Complete research and blog generation system ## Examples 1. [Basic Agent](01_basic_agent.py) - A simple example of how to create and run an agent. 2. [Multi-Agent](02_multi_agent.py) - An example of how to create and run multiple agents. 3. [Synchronous Agent](03_sync_agent.py) - An example of how to run an agent synchronously. 4. [Agent with Tracing](04_agent_with_tracing.py) - An example of how to use tracing with agents. 5. [Agent with Function Tools](05_agent_with_function_tools.py) - An example of how to use function tools with agents. 6. [Agent with Custom Tools](06_agent_with_custom_tools.py) - An example of how to create custom tools for agents. 7. [Agent with Handoffs](07_agent_with_handoffs.py) - An example of how to use handoffs between agents. 8. [Agent with Agent as Tool](08_agent_with_agent_as_tool.py) - An example of how to use an agent as a tool for another agent. 9. [Agent with Context Management](09_agent_with_context_management.py) - An example of how to use context management with agents. 10. [Agent with Guardrails](10_agent_with_guardrails.py) - An example of how to use guardrails with agents. 11. [Agent Orchestration](11_agent_orchestration.py) - An example of how to orchestrate multiple agents for complex tasks. 12. [Anthropic Agent](12_anthropic_agent.py) - An example of how to use Anthropic's Claude model with the Agents SDK. 13. [Research Blog System](13_research_blog_system.py) - A complete system where research agents and blog agents work together. ## Usage Each example is a self-contained single file that can be run using uv: ```bash uv run example_name.py --prompt "Your prompt here" ``` You can also run all examples with the provided test script: ```bash ./test_all_examples.sh ``` ## Setup 1. Install the dependencies: `./install_dependencies.sh` 2. Set up your OpenAI API key: `export OPENAI_API_KEY=your_key_here` 3. For Anthropic examples, set up your Anthropic API key: `export ANTHROPIC_API_KEY=your_key_here` ## Requirements - Python 3.10+ - uv package manager - OpenAI API key - Anthropic API key (for cross-provider examples) ## Testing All examples include tests that can be run with: ```bash uv run pytest example_name.py ``` ## Important Implementation Notes - The OpenAI Agents SDK is installed via the `openai-agents` package but imported as `agents` - Agent execution is handled through `Runner.run()` for async and `Runner.run_sync()` for sync operations - Function tools cannot have default parameter values in their definitions - The `RunResult` object has a `final_output` attribute instead of `output` - All examples use GPT-4o-mini as the primary model for non-web search functionality - Each example includes comprehensive docstrings and comments for clarity ## Documentation For more information about the OpenAI Agents SDK, see the [official documentation](https://openai.github.io/openai-agents-python/). ================================================ FILE: openai-agents-examples/fix_imports.py ================================================ #!/usr/bin/env python3 """ Script to fix imports in all example files. """ import os import re import glob import sys def fix_imports_in_file(file_path): """Fix imports in a single file.""" with open(file_path, 'r') as f: content = f.read() # First fix the Runner.run syntax error if 'from agents import Agent, Runner.run' in content: content = content.replace('from agents import Agent, Runner.run', 'from agents import Agent, Runner') if 'from agents import Agent, Runner.run_sync' in content: content = content.replace('from agents import Agent, Runner.run_sync', 'from agents import Agent, Runner') # Replace incorrect imports with correct ones based on documentation replacements = [ ('from openai.agents import', 'from agents import'), ('import openai.agents', 'import agents'), ('from openai_agents import', 'from agents import'), ('import openai_agents', 'import agents'), ('from agents import Agent, run_agent', 'from agents import Agent, Runner'), ('from agents import Agent, run_agent_sync', 'from agents import Agent, Runner'), ('result = run_agent_sync', 'result = Runner.run_sync'), ('result = run_agent', 'result = Runner.run'), ('result = await run_agent_sync', 'result = await Runner.run_sync'), ('result = await run_agent', 'result = await Runner.run'), ('result.output', 'result.final_output'), ('return response, context', 'return result.final_output, result.context'), ('responses.append(response)', 'responses.append(result.final_output)'), ] new_content = content for old, new in replacements: new_content = new_content.replace(old, new) # Also update dependencies in the script header if '# dependencies = [' in new_content: # Update to use the correct package name and import path new_content = new_content.replace( '"openai-agents>=0.0.2",', '"openai>=1.66.0", # Includes agents module' ) new_content = new_content.replace( '"openai>=1.66.0", # Includes agents module', '"openai>=1.66.0", # Includes agents module' ) if new_content != content: with open(file_path, 'w') as f: f.write(new_content) print(f"Fixed imports in {file_path}") else: print(f"No changes needed in {file_path}") def create_agents_symlink(): """Create a symlink from agents to openai.agents if needed.""" try: import openai if hasattr(openai, 'agents'): # Create a symlink in site-packages site_packages = next(p for p in sys.path if 'site-packages' in p) agents_path = os.path.join(site_packages, 'agents') if not os.path.exists(agents_path): os.symlink(os.path.join(site_packages, 'openai', 'agents'), agents_path) print(f"Created symlink from {agents_path} to openai.agents") else: print(f"Agents path already exists at {agents_path}") except (ImportError, StopIteration, OSError) as e: print(f"Could not create symlink: {e}") def main(): """Fix imports in all Python files in the directory.""" # Try to create a symlink for agents create_agents_symlink() # Get all Python files py_files = glob.glob('*.py') for file_path in py_files: if file_path != 'fix_imports.py': # Skip this script fix_imports_in_file(file_path) print("Import fixing complete!") if __name__ == "__main__": main() ================================================ FILE: openai-agents-examples/install_dependencies.sh ================================================ #!/bin/bash # Install all required dependencies for the examples pip install openai-agents rich pytest markdown anthropic opentelemetry-api opentelemetry-sdk pydantic requests # Set up environment variables if not already set if [ -z "$OPENAI_API_KEY" ]; then echo "Warning: OPENAI_API_KEY environment variable not set" echo "Please set it with: export OPENAI_API_KEY=your_key_here" fi if [ -z "$ANTHROPIC_API_KEY" ]; then echo "Warning: ANTHROPIC_API_KEY environment variable not set" echo "Please set it with: export ANTHROPIC_API_KEY=your_key_here" fi echo "Dependencies installed successfully!" ================================================ FILE: openai-agents-examples/summary.md ================================================ # OpenAI Agents SDK Examples Summary ## Overview This repository contains 13 examples demonstrating various features of the OpenAI Agents SDK. ## Key Learnings 1. The OpenAI Agents SDK is installed via the 'openai-agents' package but imported as 'agents' 2. Agent execution is handled through Runner.run() for async and Runner.run_sync() for sync operations 3. Function tools cannot have default parameter values in their definitions 4. The RunResult object has a final_output attribute instead of output 5. The SDK supports various capabilities including multi-agent systems, tracing, and guardrails ## Testing All examples include built-in tests that can be run with pytest: ```bash uv run pytest example_name.py ``` ## Running Examples Each example can be run using uv: ```bash uv run example_name.py --prompt "Your prompt here" ``` ## Environment Setup 1. Install dependencies: `./install_dependencies.sh` 2. Set up API keys: `export OPENAI_API_KEY=your_key_here` ## Documentation For more information, see the [official documentation](https://openai.github.io/openai-agents-python/). ================================================ FILE: openai-agents-examples/test_all_examples.sh ================================================ #!/bin/bash # Set up environment export OPENAI_API_KEY=$GenAI_Keys_OPENAI_API_KEY export ANTHROPIC_API_KEY=$GenAI_Keys_ANTHROPIC_API_KEY # Test basic examples echo "Testing 01_basic_agent.py..." uv run 01_basic_agent.py --prompt "What is 2+2?" echo "Testing 02_multi_agent.py..." uv run 02_multi_agent.py --prompt "What are the benefits of exercise?" echo "Testing 03_sync_agent.py..." uv run 03_sync_agent.py --prompt "Tell me a fun fact" echo "Testing 04_agent_with_tracing.py..." uv run 04_agent_with_tracing.py --prompt "What is the capital of France?" echo "Testing 05_agent_with_function_tools.py..." uv run 05_agent_with_function_tools.py --prompt "What's the weather in New York?" echo "Testing 06_agent_with_custom_tools.py..." uv run 06_agent_with_custom_tools.py --prompt "Calculate 15% tip on a $75 bill" echo "Testing 07_agent_with_handoffs.py..." uv run 07_agent_with_handoffs.py --prompt "I need help with a coding problem" echo "Testing 08_agent_with_agent_as_tool.py..." uv run 08_agent_with_agent_as_tool.py --prompt "Tell me about climate change" echo "Testing 09_agent_with_context_management.py..." uv run 09_agent_with_context_management.py --prompt "Tell me about Mars" --follow-up "What about its moons?" echo "Testing 10_agent_with_guardrails.py..." uv run 10_agent_with_guardrails.py --prompt "Tell me about renewable energy" echo "Testing 11_agent_orchestration.py..." uv run 11_agent_orchestration.py --prompt "Write a short blog post about AI" echo "Testing 12_anthropic_agent.py..." uv run 12_anthropic_agent.py --prompt "What is your favorite book?" echo "Testing 13_research_blog_system.py..." uv run 13_research_blog_system.py --topic "Space Exploration" --output "space_blog.md" echo "All tests completed successfully!" ================================================ FILE: openai-agents-examples/test_imports.py ================================================ #!/usr/bin/env python3 """ Test script to check the correct import name for OpenAI Agents SDK. """ try: import agents print("Successfully imported as 'openai_agents'") except ImportError: print("Failed to import as 'openai_agents'") try: import agents print("Successfully imported as 'openai.agents'") except ImportError: print("Failed to import as 'openai.agents'") try: import agents.agent print("Successfully imported as 'openai_agents.agent'") except ImportError: print("Failed to import as 'openai_agents.agent'") try: import agents.agent print("Successfully imported as 'openai.agents.agent'") except ImportError: print("Failed to import as 'openai.agents.agent'") ================================================ FILE: sfa_bash_editor_agent_anthropic_v2.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # "anthropic>=0.45.2", # "rich>=13.7.0", # ] # /// """ Usage: # View a file uv run sfa_bash_editor_agent_anthropic_v2.py --prompt "Show me the first 10 lines of README.md" # Create a new file uv run sfa_bash_editor_agent_anthropic_v2.py --prompt "Create a new file called hello.txt with 'Hello World!' in it" # Replace text in a file uv run sfa_bash_editor_agent_anthropic_v2.py --prompt "Create a new file called hello.txt with 'Hello World!' in it. Then update hello.txt to say 'Hello AI Coding World'" # Insert a line in a file uv run sfa_bash_editor_agent_anthropic_v2.py --prompt "Create a new file called hello2.txt with 'Hello AI Coding World!' in it. Then add a new line 'How are you?' after 'Hello AI World!' in hello.txt" # Execute a bash command uv run sfa_bash_editor_agent_anthropic_v2.py --prompt "List all Python files in the current directory sorted by size" # Complete a multi-step task uv run sfa_bash_editor_agent_anthropic_v2.py --prompt "List all Python files in the current directory sorted by size, then output to a markdown file called python_files_sorted_by_size.md" """ import os import sys import argparse import json import traceback from rich.console import Console from rich.panel import Panel import anthropic # Initialize global console console = Console() current_bash_env = os.environ.copy() AGENT_PROMPT = """ You are an expert integration assistant that can both edit files and execute bash commands. Use the tools provided to accomplish file editing and bash command execution as needed. When you have completed the user's task, call complete_task to finalize the process. Provide reasoning with every tool call. When constructing paths use /repo to start from the root of the repository. We'll replace it with the current working directory. view_file View the content of a file reasoning string Why you are viewing the file true path string Path of the file to view true create_file Create a new file with given content reasoning string Why the file is being created true path string Path where to create the file true file_text string Content for the new file true str_replace Replace text in a file reasoning string Explain why the replacement is needed true path string File path true old_str string The string to be replaced true new_str string The replacement string true insert_line Insert text at a specific line in a file reasoning string Reason for inserting the text true path string File path true insert_line integer Line number for insertion true new_str string The text to insert true execute_bash Execute a bash command reasoning string Explain why this command should be executed true command string The bash command to run true restart_bash Restart the bash session with a fresh environment reasoning string Explain why the session is being reset true complete_task Finalize the task and exit the agent loop reasoning string Explain why the task is complete true {{user_request}} """ root_path_to_replace_with_cwd = "/repo" def tool_view_file(tool_input: dict) -> dict: try: reasoning = tool_input.get("reasoning") path = tool_input.get("path") path = path.replace(root_path_to_replace_with_cwd, os.getcwd()) if not path or not path.strip(): error_message = "Invalid file path provided: path is empty." console.log(f"[tool_view_file] Error: {error_message}") return {"error": error_message} console.log(f"[tool_view_file] reasoning: {reasoning}, path: {path}") if not os.path.exists(path): error_message = f"File {path} does not exist" console.log(f"[tool_view_file] Error: {error_message}") return {"error": error_message} with open(path, "r") as f: content = f.read() return {"result": content} except Exception as e: console.log(f"[tool_view_file] Error: {str(e)}") console.log(traceback.format_exc()) return {"error": str(e)} def tool_create_file(tool_input: dict) -> dict: try: reasoning = tool_input.get("reasoning") path = tool_input.get("path") file_text = tool_input.get("file_text") path = path.replace(root_path_to_replace_with_cwd, os.getcwd()) console.log(f"[tool_create_file] reasoning: {reasoning}, path: {path}") # Check for an empty or invalid path if not path or not path.strip(): error_message = "Invalid file path provided: path is empty." console.log(f"[tool_create_file] Error: {error_message}") return {"error": error_message} dirname = os.path.dirname(path) if not dirname: error_message = ( "Invalid file path provided: directory part of the path is empty." ) console.log(f"[tool_create_file] Error: {error_message}") return {"error": error_message} else: os.makedirs(dirname, exist_ok=True) with open(path, "w") as f: f.write(file_text) return {"result": f"File created at {path}"} except Exception as e: console.log(f"[tool_create_file] Error: {str(e)}") console.log(traceback.format_exc()) return {"error": str(e)} def tool_str_replace(tool_input: dict) -> dict: try: reasoning = tool_input.get("reasoning") path = tool_input.get("path") old_str = tool_input.get("old_str") new_str = tool_input.get("new_str") path = path.replace(root_path_to_replace_with_cwd, os.getcwd()) if not path or not path.strip(): error_message = "Invalid file path provided: path is empty." console.log(f"[tool_str_replace] Error: {error_message}") return {"error": error_message} if not old_str: error_message = "No text to replace specified: old_str is empty." console.log(f"[tool_str_replace] Error: {error_message}") return {"error": error_message} console.log( f"[tool_str_replace] reasoning: {reasoning}, path: {path}, old_str: {old_str}, new_str: {new_str}" ) if not os.path.exists(path): error_message = f"File {path} does not exist" console.log(f"[tool_str_replace] Error: {error_message}") return {"error": error_message} with open(path, "r") as f: content = f.read() if old_str not in content: error_message = f"'{old_str}' not found in {path}" console.log(f"[tool_str_replace] Error: {error_message}") return {"error": error_message} new_content = content.replace(old_str, new_str) with open(path, "w") as f: f.write(new_content) return {"result": "Text replaced successfully"} except Exception as e: console.log(f"[tool_str_replace] Error: {str(e)}") console.log(traceback.format_exc()) return {"error": str(e)} def tool_insert_line(tool_input: dict) -> dict: try: reasoning = tool_input.get("reasoning") path = tool_input.get("path") insert_line_num = tool_input.get("insert_line") new_str = tool_input.get("new_str") path = path.replace(root_path_to_replace_with_cwd, os.getcwd()) if not path or not path.strip(): error_message = "Invalid file path provided: path is empty." console.log(f"[tool_insert_line] Error: {error_message}") return {"error": error_message} if insert_line_num is None: error_message = "No line number specified: insert_line is missing." console.log(f"[tool_insert_line] Error: {error_message}") return {"error": error_message} if not new_str: error_message = "No text to insert specified: new_str is empty." console.log(f"[tool_insert_line] Error: {error_message}") return {"error": error_message} console.log( f"[tool_insert_line] reasoning: {reasoning}, path: {path}, insert_line: {insert_line_num}, new_str: {new_str}" ) if not os.path.exists(path): error_message = f"File {path} does not exist" console.log(f"[tool_insert_line] Error: {error_message}") return {"error": error_message} with open(path, "r") as f: lines = f.readlines() # Check that the index is within acceptable bounds (allowing insertion at end) if insert_line_num < 0 or insert_line_num > len(lines): error_message = ( f"Insert line number {insert_line_num} out of range (0-{len(lines)})." ) console.log(f"[tool_insert_line] Error: {error_message}") return {"error": error_message} lines.insert(insert_line_num, new_str + "\n") with open(path, "w") as f: f.writelines(lines) return {"result": "Line inserted successfully"} except Exception as e: console.log(f"[tool_insert_line] Error: {str(e)}") console.log(traceback.format_exc()) return {"error": str(e)} def tool_execute_bash(tool_input: dict) -> dict: try: reasoning = tool_input.get("reasoning") command = tool_input.get("command") command = command.replace(root_path_to_replace_with_cwd, os.getcwd()) if not command or not command.strip(): error_message = "No command specified: command is empty." console.log(f"[tool_execute_bash] Error: {error_message}") return {"error": error_message} console.log(f"[tool_execute_bash] reasoning: {reasoning}, command: {command}") import subprocess result = subprocess.run( command, shell=True, capture_output=True, text=True, env=current_bash_env ) if result.returncode != 0: error_message = ( result.stderr.strip() or "Command execution failed with non-zero exit code." ) console.log(f"[tool_execute_bash] Error: {error_message}") return {"error": error_message} return {"result": result.stdout.strip()} except Exception as e: console.log(f"[tool_execute_bash] Error: {str(e)}") console.log(traceback.format_exc()) return {"error": str(e)} def tool_restart_bash(tool_input: dict) -> dict: global current_bash_env try: reasoning = tool_input.get("reasoning") if not reasoning: error_message = "No reasoning provided for restarting bash session." console.log(f"[tool_restart_bash] Error: {error_message}") return {"error": error_message} console.log(f"[tool_restart_bash] reasoning: {reasoning}") current_bash_env = os.environ.copy() return {"result": "Bash session restarted."} except Exception as e: console.log(f"[tool_restart_bash] Error: {str(e)}") console.log(traceback.format_exc()) return {"error": str(e)} def tool_complete_task(tool_input: dict) -> dict: try: reasoning = tool_input.get("reasoning") if not reasoning: error_message = "No reasoning provided for task completion." console.log(f"[tool_complete_task] Error: {error_message}") return {"error": error_message} console.log(f"[tool_complete_task] reasoning: {reasoning}") return {"result": "Task completed"} except Exception as e: console.log(f"[tool_complete_task] Error: {str(e)}") console.log(traceback.format_exc()) return {"error": str(e)} def main(): parser = argparse.ArgumentParser( description="Bash and Editor Agent using Anthropic API" ) parser.add_argument("-p", "--prompt", required=True, help="The prompt to execute") parser.add_argument( "-c", "--compute", type=int, default=10, help="Maximum compute loops" ) args = parser.parse_args() ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY") if not ANTHROPIC_API_KEY: Console().print( "[red]Error: ANTHROPIC_API_KEY environment variable is not set.[/red]" ) sys.exit(1) client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY) # Prepare the initial message using the detailed prompt initial_message = AGENT_PROMPT.replace("{{user_request}}", args.prompt) messages = [{"role": "user", "content": initial_message}] compute_iterations = 0 # Begin the agent loop. # This loop processes Anthropic API responses, executes tool calls for both editor and bash commands, # and logs detailed information via rich logging. while compute_iterations < args.compute: compute_iterations += 1 console.rule(f"[yellow]Agent Loop {compute_iterations}/{args.compute}[/yellow]") try: response = client.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=1024, messages=messages, tools=[ { "name": "view_file", "description": "View the content of a file", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why view the file", }, "path": {"type": "string", "description": "File path"}, }, "required": ["reasoning", "path"], }, }, { "name": "create_file", "description": "Create a new file with given content", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why create the file", }, "path": {"type": "string", "description": "File path"}, "file_text": { "type": "string", "description": "Content for the file", }, }, "required": ["reasoning", "path", "file_text"], }, }, { "name": "str_replace", "description": "Replace text in a file", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Reason for replacement", }, "path": {"type": "string", "description": "File path"}, "old_str": { "type": "string", "description": "Text to replace", }, "new_str": { "type": "string", "description": "Replacement text", }, }, "required": ["reasoning", "path", "old_str", "new_str"], }, }, { "name": "insert_line", "description": "Insert text at a specific line in a file", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Reason for insertion", }, "path": {"type": "string", "description": "File path"}, "insert_line": { "type": "integer", "description": "Line number", }, "new_str": { "type": "string", "description": "Text to insert", }, }, "required": ["reasoning", "path", "insert_line", "new_str"], }, }, { "name": "execute_bash", "description": "Execute a bash command", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Reason for command execution", }, "command": { "type": "string", "description": "Bash command", }, }, "required": ["reasoning", "command"], }, }, { "name": "restart_bash", "description": "Restart the bash session with fresh environment", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why to restart bash", } }, "required": ["reasoning"], }, }, { "name": "complete_task", "description": "Complete the task and exit the agent loop", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why the task is complete", } }, "required": ["reasoning"], }, }, ], tool_choice={"type": "any"}, ) except Exception as e: console.print(f"[red]Error in API call: {str(e)}[/red]") console.print(traceback.format_exc()) break console.log("[green]API Response:[/green]", response.model_dump()) tool_calls = [ block for block in response.content if hasattr(block, "type") and block.type == "tool_use" ] if tool_calls: # Map tool names to their corresponding functions tool_functions = { "view_file": tool_view_file, "create_file": tool_create_file, "str_replace": tool_str_replace, "insert_line": tool_insert_line, "execute_bash": tool_execute_bash, "restart_bash": tool_restart_bash, "complete_task": tool_complete_task, } for tool in tool_calls: messages.append({"role": "assistant", "content": response.content}) console.print( f"[blue]Tool Call:[/blue] {tool.name}({json.dumps(tool.input)})" ) func = tool_functions.get(tool.name) if func: output = func(tool.input) result_text = output.get("error") or output.get("result", "") console.print(f"[green]Tool Result:[/green] {result_text}") messages.append( { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool.id, "content": result_text, } ], } ) if tool.name == "complete_task": console.print( "[green]Task completed. Exiting agent loop.[/green]" ) return else: raise ValueError(f"Unknown tool: {tool.name}") console.print("[yellow]Reached compute limit without completing task.[/yellow]") if __name__ == "__main__": main() ================================================ FILE: sfa_bash_editor_agent_anthropic_v3.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # "anthropic>=0.45.2", # "rich>=13.7.0", # ] # /// """ Usage: # View a file uv run sfa_bash_editor_agent_anthropic_v3.py --prompt "Show me the first 10 lines of README.md" # Create a new file uv run sfa_bash_editor_agent_anthropic_v3.py --prompt "Create a new file called hello.txt with 'Hello World!' in it" # Replace text in a file uv run sfa_bash_editor_agent_anthropic_v3.py --prompt "Create a new file called hello.txt with 'Hello World!' in it. Then update hello.txt to say 'Hello AI Coding World'" # Insert a line in a file uv run sfa_bash_editor_agent_anthropic_v3.py --prompt "Create a new file called hello2.txt with 'Hello AI Coding World!' in it. Then add a new line 'How are you?' after 'Hello AI World!' in hello.txt" # Execute a bash command uv run sfa_bash_editor_agent_anthropic_v3.py --prompt "List all Python files in the current directory sorted by size" # Complete a multi-step task uv run sfa_bash_editor_agent_anthropic_v3.py --prompt "List all Python files in the current directory sorted by size, then output to a markdown file called python_files_sorted_by_size.md" """ import os import sys import argparse import json import traceback from rich.console import Console from rich.panel import Panel import anthropic # Initialize global console console = Console() current_bash_env = os.environ.copy() AGENT_PROMPT = """ You are an expert integration assistant that can both edit files and execute bash commands. Use the tools provided to accomplish file editing and bash command execution as needed. When you have completed the user's task, call complete_task to finalize the process. Provide reasoning with every tool call. When constructing paths use /repo to start from the root of the repository. We'll replace it with the current working directory. view_file View the content of a file reasoning string Why you are viewing the file true path string Path of the file to view true create_file Create a new file with given content reasoning string Why the file is being created true path string Path where to create the file true file_text string Content for the new file true str_replace Replace text in a file reasoning string Explain why the replacement is needed true path string File path true old_str string The string to be replaced true new_str string The replacement string true insert_line Insert text at a specific line in a file reasoning string Reason for inserting the text true path string File path true insert_line integer Line number for insertion true new_str string The text to insert true execute_bash Execute a bash command reasoning string Explain why this command should be executed true command string The bash command to run true restart_bash Restart the bash session with a fresh environment reasoning string Explain why the session is being reset true complete_task Finalize the task and exit the agent loop reasoning string Explain why the task is complete true {{user_request}} """ root_path_to_replace_with_cwd = "/repo" def tool_view_file(tool_input: dict) -> dict: try: reasoning = tool_input.get("reasoning") path = tool_input.get("path") if path: path = path.replace(root_path_to_replace_with_cwd, os.getcwd()) if not path or not path.strip(): error_message = "Invalid file path provided: path is empty." console.log(f"[tool_view_file] Error: {error_message}") return {"error": error_message} console.log(f"[tool_view_file] reasoning: {reasoning}, path: {path}") if not os.path.exists(path): error_message = f"File {path} does not exist" console.log(f"[tool_view_file] Error: {error_message}") return {"error": error_message} with open(path, "r") as f: content = f.read() return {"result": content} except Exception as e: console.log(f"[tool_view_file] Error: {str(e)}") console.log(traceback.format_exc()) return {"error": str(e)} def tool_create_file(tool_input: dict) -> dict: try: reasoning = tool_input.get("reasoning") path = tool_input.get("path") file_text = tool_input.get("file_text") if path: path = path.replace(root_path_to_replace_with_cwd, os.getcwd()) console.log(f"[tool_create_file] reasoning: {reasoning}, path: {path}") # Check for an empty or invalid path if not path or not path.strip(): error_message = "Invalid file path provided: path is empty." console.log(f"[tool_create_file] Error: {error_message}") return {"error": error_message} dirname = os.path.dirname(path) if not dirname: error_message = ( "Invalid file path provided: directory part of the path is empty." ) console.log(f"[tool_create_file] Error: {error_message}") return {"error": error_message} else: os.makedirs(dirname, exist_ok=True) with open(path, "w") as f: f.write(file_text or "") return {"result": f"File created at {path}"} except Exception as e: console.log(f"[tool_create_file] Error: {str(e)}") console.log(traceback.format_exc()) return {"error": str(e)} def tool_str_replace(tool_input: dict) -> dict: try: reasoning = tool_input.get("reasoning") path = tool_input.get("path") old_str = tool_input.get("old_str") new_str = tool_input.get("new_str") if path: path = path.replace(root_path_to_replace_with_cwd, os.getcwd()) if not path or not path.strip(): error_message = "Invalid file path provided: path is empty." console.log(f"[tool_str_replace] Error: {error_message}") return {"error": error_message} if not old_str: error_message = "No text to replace specified: old_str is empty." console.log(f"[tool_str_replace] Error: {error_message}") return {"error": error_message} console.log( f"[tool_str_replace] reasoning: {reasoning}, path: {path}, old_str: {old_str}, new_str: {new_str}" ) if not os.path.exists(path): error_message = f"File {path} does not exist" console.log(f"[tool_str_replace] Error: {error_message}") return {"error": error_message} with open(path, "r") as f: content = f.read() if old_str not in content: error_message = f"'{old_str}' not found in {path}" console.log(f"[tool_str_replace] Error: {error_message}") return {"error": error_message} new_content = content.replace(old_str, new_str or "") with open(path, "w") as f: f.write(new_content) return {"result": "Text replaced successfully"} except Exception as e: console.log(f"[tool_str_replace] Error: {str(e)}") console.log(traceback.format_exc()) return {"error": str(e)} def tool_insert_line(tool_input: dict) -> dict: try: reasoning = tool_input.get("reasoning") path = tool_input.get("path") insert_line_num = tool_input.get("insert_line") new_str = tool_input.get("new_str") if path: path = path.replace(root_path_to_replace_with_cwd, os.getcwd()) if not path or not path.strip(): error_message = "Invalid file path provided: path is empty." console.log(f"[tool_insert_line] Error: {error_message}") return {"error": error_message} if insert_line_num is None: error_message = "No line number specified: insert_line is missing." console.log(f"[tool_insert_line] Error: {error_message}") return {"error": error_message} if not new_str: error_message = "No text to insert specified: new_str is empty." console.log(f"[tool_insert_line] Error: {error_message}") return {"error": error_message} console.log( f"[tool_insert_line] reasoning: {reasoning}, path: {path}, insert_line: {insert_line_num}, new_str: {new_str}" ) if not os.path.exists(path): error_message = f"File {path} does not exist" console.log(f"[tool_insert_line] Error: {error_message}") return {"error": error_message} with open(path, "r") as f: lines = f.readlines() # Check that the index is within acceptable bounds (allowing insertion at end) if insert_line_num < 0 or insert_line_num > len(lines): error_message = ( f"Insert line number {insert_line_num} out of range (0-{len(lines)})." ) console.log(f"[tool_insert_line] Error: {error_message}") return {"error": error_message} lines.insert(insert_line_num, new_str + "\n") with open(path, "w") as f: f.writelines(lines) return {"result": "Line inserted successfully"} except Exception as e: console.log(f"[tool_insert_line] Error: {str(e)}") console.log(traceback.format_exc()) return {"error": str(e)} def tool_execute_bash(tool_input: dict) -> dict: try: reasoning = tool_input.get("reasoning") command = tool_input.get("command") if command: command = command.replace(root_path_to_replace_with_cwd, os.getcwd()) if not command or not command.strip(): error_message = "No command specified: command is empty." console.log(f"[tool_execute_bash] Error: {error_message}") return {"error": error_message} console.log(f"[tool_execute_bash] reasoning: {reasoning}, command: {command}") import subprocess result = subprocess.run( command, shell=True, capture_output=True, text=True, env=current_bash_env ) if result.returncode != 0: error_message = ( result.stderr.strip() or "Command execution failed with non-zero exit code." ) console.log(f"[tool_execute_bash] Error: {error_message}") return {"error": error_message} return {"result": result.stdout.strip()} except Exception as e: console.log(f"[tool_execute_bash] Error: {str(e)}") console.log(traceback.format_exc()) return {"error": str(e)} def tool_restart_bash(tool_input: dict) -> dict: global current_bash_env try: reasoning = tool_input.get("reasoning") if not reasoning: error_message = "No reasoning provided for restarting bash session." console.log(f"[tool_restart_bash] Error: {error_message}") return {"error": error_message} console.log(f"[tool_restart_bash] reasoning: {reasoning}") current_bash_env = os.environ.copy() return {"result": "Bash session restarted."} except Exception as e: console.log(f"[tool_restart_bash] Error: {str(e)}") console.log(traceback.format_exc()) return {"error": str(e)} def tool_complete_task(tool_input: dict) -> dict: try: reasoning = tool_input.get("reasoning") if not reasoning: error_message = "No reasoning provided for task completion." console.log(f"[tool_complete_task] Error: {error_message}") return {"error": error_message} console.log(f"[tool_complete_task] reasoning: {reasoning}") return {"result": "Task completed"} except Exception as e: console.log(f"[tool_complete_task] Error: {str(e)}") console.log(traceback.format_exc()) return {"error": str(e)} def main(): parser = argparse.ArgumentParser( description="Bash and Editor Agent using Anthropic API" ) parser.add_argument("-p", "--prompt", required=True, help="The prompt to execute") parser.add_argument( "-c", "--compute", type=int, default=10, help="Maximum compute loops" ) args = parser.parse_args() ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY") if not ANTHROPIC_API_KEY: Console().print( "[red]Error: ANTHROPIC_API_KEY environment variable is not set.[/red]" ) sys.exit(1) client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY) # Prepare the initial message using the detailed prompt initial_message = AGENT_PROMPT.replace("{{user_request}}", args.prompt) messages = [{"role": "user", "content": initial_message}] compute_iterations = 0 # Define tools for the agent tools = [ { "name": "view_file", "description": "View the content of a file", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why view the file", }, "path": {"type": "string", "description": "File path"}, }, "required": ["reasoning", "path"], }, }, { "name": "create_file", "description": "Create a new file with given content", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why create the file", }, "path": {"type": "string", "description": "File path"}, "file_text": { "type": "string", "description": "Content for the file", }, }, "required": ["reasoning", "path", "file_text"], }, }, { "name": "str_replace", "description": "Replace text in a file", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Reason for replacement", }, "path": {"type": "string", "description": "File path"}, "old_str": { "type": "string", "description": "Text to replace", }, "new_str": { "type": "string", "description": "Replacement text", }, }, "required": ["reasoning", "path", "old_str", "new_str"], }, }, { "name": "insert_line", "description": "Insert text at a specific line in a file", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Reason for insertion", }, "path": {"type": "string", "description": "File path"}, "insert_line": { "type": "integer", "description": "Line number", }, "new_str": { "type": "string", "description": "Text to insert", }, }, "required": ["reasoning", "path", "insert_line", "new_str"], }, }, { "name": "execute_bash", "description": "Execute a bash command", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Reason for command execution", }, "command": { "type": "string", "description": "Bash command", }, }, "required": ["reasoning", "command"], }, }, { "name": "restart_bash", "description": "Restart the bash session with fresh environment", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why to restart bash", } }, "required": ["reasoning"], }, }, { "name": "complete_task", "description": "Complete the task and exit the agent loop", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why the task is complete", } }, "required": ["reasoning"], }, }, ] # Begin the agent loop. # This loop processes Anthropic API responses, executes tool calls for both editor and bash commands, # and logs detailed information via rich logging. while compute_iterations < args.compute: compute_iterations += 1 console.rule(f"[yellow]Agent Loop {compute_iterations}/{args.compute}[/yellow]") try: response = client.messages.create( model="claude-3-7-sonnet-20250219", max_tokens=4000, # Increased to be greater than thinking.budget_tokens thinking={ "type": "enabled", "budget_tokens": 2000 # Using 2k thinking tokens as requested }, messages=messages, tools=tools, ) except Exception as e: console.print(f"[red]Error in API call: {str(e)}[/red]") console.print(traceback.format_exc()) break console.log("[green]API Response:[/green]", response.model_dump()) tool_calls = [ block for block in response.content if hasattr(block, "type") and block.type == "tool_use" ] if tool_calls: # Map tool names to their corresponding functions tool_functions = { "view_file": tool_view_file, "create_file": tool_create_file, "str_replace": tool_str_replace, "insert_line": tool_insert_line, "execute_bash": tool_execute_bash, "restart_bash": tool_restart_bash, "complete_task": tool_complete_task, } # Add the assistant's response to messages messages.append({"role": "assistant", "content": response.content}) for tool in tool_calls: console.print( f"[blue]Tool Call:[/blue] {tool.name}({json.dumps(tool.input)})" ) func = tool_functions.get(tool.name) if func: output = func(tool.input) result_text = output.get("error") or output.get("result", "") console.print(f"[green]Tool Result:[/green] {result_text}") # Format the tool result message according to Claude API requirements tool_result_message = { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool.id, "content": result_text } ] } messages.append(tool_result_message) if tool.name == "complete_task": console.print( "[green]Task completed. Exiting agent loop.[/green]" ) return else: raise ValueError(f"Unknown tool: {tool.name}") console.print("[yellow]Reached compute limit without completing task.[/yellow]") if __name__ == "__main__": main() ================================================ FILE: sfa_codebase_context_agent_v3.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # "anthropic>=0.47.1", # "rich>=13.7.0", # "pydantic>=2.0.0", # ] # /// """ Usage: uv run sfa_codebase_context_agent_v3.py \ --prompt "Let's build a new metaprompt sfa agent using anthropic claude 3.7" \ --directory "." \ --globs "*.py" \ --extensions py md \ --limit 10 \ --file-line-limit 1000 \ --output-file relevant_files.json \ --compute 15 # Find files related to DuckDB implementations uv run sfa_codebase_context_agent_v3.py \ --prompt "Find all files related to DuckDB agent implementations" \ --file-line-limit 1000 \ --extensions py # Find all files related to Anthropic-powered agents uv run sfa_codebase_context_agent_v3.py \ --prompt "Identify all agents that use the new Claude 3.7 model" """ import os import sys import json import argparse import subprocess import time import fnmatch import concurrent.futures from typing import List, Dict, Any from rich.console import Console from anthropic import Anthropic from rich.table import Table from rich.panel import Panel # Initialize rich console console = Console() # Constants THINKING_BUDGET_TOKENS_PER_FILE = 2000 BATCH_SIZE = 10 MAX_RETRIES = 3 RETRY_WAIT = 1 # Global variables USER_PROMPT = "" RELEVANT_FILES = [] OUTPUT_FILE = "output_relevant_files.json" INPUT_TOKENS = 0 # To track input tokens to Anthropic API OUTPUT_TOKENS = 0 # To track output tokens from Anthropic API def git_list_files( reasoning: str, directory: str = os.getcwd(), globs: List[str] = [], extensions: List[str] = [], ) -> List[str]: """Returns a list of files in the repository, respecting gitignore. Args: reasoning: Explanation of why we're listing files directory: Directory to search in (defaults to current working directory) globs: List of glob patterns to filter files (optional) extensions: List of file extensions to filter files (optional) Returns: List of file paths as strings """ try: console.log(f"[blue]Git List Files Tool[/blue] - Reasoning: {reasoning}") console.log( f"[dim]Directory: {directory}, Globs: {globs}, Extensions: {extensions}[/dim]" ) # Change to the specified directory original_dir = os.getcwd() os.chdir(directory) # Get all files tracked by git result = subprocess.run( "git ls-files", shell=True, text=True, capture_output=True, ) files = result.stdout.strip().split("\n") # Filter by globs if provided if globs: filtered_files = [] for pattern in globs: for file in files: if fnmatch.fnmatch(file, pattern): filtered_files.append(file) files = filtered_files # Filter by extensions if provided if extensions: files = [ file for file in files if any(file.endswith(f".{ext}") for ext in extensions) ] # Change back to the original directory os.chdir(original_dir) # # Convert to absolute paths # files = [os.path.join(directory, file) for file in files] # Keep paths relative files = files console.log(f"[dim]Found {len(files)} files[/dim]") return files except Exception as e: console.log(f"[red]Error listing files: {str(e)}[/red]") return [] def check_file_paths_line_length( reasoning: str, file_paths: List[str], file_line_limit: int = 500 ) -> Dict[str, int]: """Checks the line length of each file and returns a dictionary of file paths and their line counts. Args: reasoning: Explanation of why we're checking line lengths file_paths: List of file paths to check file_line_limit: Maximum number of lines per file Returns: Dictionary mapping file paths to their total line counts """ try: console.log( f"[blue]Check File Paths Line Length Tool[/blue] - Reasoning: {reasoning}" ) console.log( f"[dim]Checking {len(file_paths)} files with line limit {file_line_limit}[/dim]" ) result = {} for file_path in file_paths: try: with open(file_path, "r", encoding="utf-8") as f: lines = f.readlines() line_count = len(lines) if line_count <= file_line_limit: result[file_path] = line_count else: console.log( f"[yellow]Skipping {file_path}: {line_count} lines exceed limit of {file_line_limit}[/yellow]" ) except Exception as e: console.log(f"[red]Error reading file {file_path}: {str(e)}[/red]") console.log(f"[dim]Found {len(result)} files within line limit[/dim]") return result except Exception as e: console.log(f"[red]Error checking file paths: {str(e)}[/red]") return {} def determine_if_file_is_relevant(prompt: str, file_path: str, client: Anthropic) -> Dict[str, Any]: # type: ignore """Determines if a single file is relevant to the prompt. Args: prompt: The user prompt file_path: Path to the file to check client: Anthropic client Returns: Dictionary with reasoning and is_relevant flag """ result = { "reasoning": "Error: Could not process file", "file_path": file_path, "is_relevant": False, } try: with open(file_path, "r", encoding="utf-8") as f: file_content = f.read() # Truncate file content if it's too long if len(file_content) > 10000: file_content = file_content[:10000] + "... [content truncated]" file_prompt = f""" You are a codebase context builder. Your task is to determine if a file is relevant to a user query. Analyze the file content and determine if it's relevant to the user query. Provide clear reasoning for your decision. Return a structured output with your reasoning and a boolean indicating relevance. Resond in JSON format following the json-output-format. {prompt} {file_path} {file_content} {{ "reasoning": "Explanation of why the file is relevant", "is_relevant": true | false }} """ for attempt in range(MAX_RETRIES): try: response = client.messages.create( model="claude-3-7-sonnet-20250219", max_tokens=3000, # Increased to be greater than thinking.budget_tokens thinking={ "type": "enabled", "budget_tokens": THINKING_BUDGET_TOKENS_PER_FILE, }, messages=[{"role": "user", "content": file_prompt}], system="Determine if the file is relevant to the user query.", ) # Track token usage global INPUT_TOKENS, OUTPUT_TOKENS if hasattr(response, 'usage') and response.usage: INPUT_TOKENS += response.usage.input_tokens OUTPUT_TOKENS += response.usage.output_tokens # Parse the response - look for text blocks response_text = None # Loop through all content blocks to find the text block for content_block in response.content: if content_block.type == "text": response_text = content_block.text break # Make sure we have a text response if response_text is None: raise Exception("No text response found in the model output") # Handle different response formats try: # Try parsing as JSON first result = json.loads(response_text) except json.JSONDecodeError: # If not valid JSON, try to extract reasoning and is_relevant from text is_relevant = "relevant" in response_text.lower() and not ( "not relevant" in response_text.lower() ) result = { "reasoning": response_text.strip(), "is_relevant": is_relevant, } return { "reasoning": result.get("reasoning", "No reasoning provided"), "file_path": file_path, "is_relevant": result.get("is_relevant", False), } except Exception as e: if attempt < MAX_RETRIES - 1: console.log( f"[yellow]Retry {attempt + 1}/{MAX_RETRIES} for {file_path}: {str(e)}[/yellow]" ) time.sleep(RETRY_WAIT) else: console.log( f"[red]Failed to determine relevance for {file_path}: {str(e)}[/red]" ) return { "reasoning": f"Error: {str(e)}", "file_path": file_path, "is_relevant": False, } except Exception as e: console.log(f"[red]Error processing file {file_path}: {str(e)}[/red]") return { "reasoning": f"Error: {str(e)}", "file_path": file_path, "is_relevant": False, } def determine_if_files_are_relevant( reasoning: str, file_paths: List[str] ) -> Dict[str, Any]: """Determines if files are relevant to the prompt using parallelism. Args: reasoning: Explanation of why we're determining relevance file_paths: List of file paths to check Returns: Dictionary with results for each file """ try: console.log( f"[blue]Determine If Files Are Relevant Tool[/blue] - Reasoning: {reasoning}" ) console.log( f"[dim]Checking {len(file_paths)} files in batches of {BATCH_SIZE}[/dim]" ) # Initialize Anthropic client client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY")) results = {} # Process files in batches for i in range(0, len(file_paths), BATCH_SIZE): batch = file_paths[i : i + BATCH_SIZE] console.log( f"[dim]Processing batch {i//BATCH_SIZE + 1}/{(len(file_paths) + BATCH_SIZE - 1)//BATCH_SIZE}[/dim]" ) # Process batch in parallel with concurrent.futures.ThreadPoolExecutor( max_workers=BATCH_SIZE ) as executor: future_to_file = { executor.submit( determine_if_file_is_relevant, USER_PROMPT, file_path, client ): file_path for file_path in batch } for future in concurrent.futures.as_completed(future_to_file): file_path = future_to_file[future] try: result = future.result() results[file_path] = result relevance = ( "Relevant" if result["is_relevant"] else "Not relevant" ) console.log(f"[dim]{file_path}: {relevance}[/dim]") except Exception as e: console.log( f"[red]Error processing {file_path}: {str(e)}[/red]" ) return results except Exception as e: console.log(f"[red]Error determining file relevance: {str(e)}[/red]") return {} def add_relevant_files(reasoning: str, file_paths: List[str]) -> str: """Adds files to the list of relevant files. Args: reasoning: Explanation of why we're adding these files file_paths: List of file paths to add Returns: String indicating success """ try: console.log(f"[blue]Add Relevant Files Tool[/blue] - Reasoning: {reasoning}") console.log(f"[dim]Adding {len(file_paths)} files to relevant files list[/dim]") global RELEVANT_FILES for file_path in file_paths: if file_path not in RELEVANT_FILES: RELEVANT_FILES.append(file_path) console.log( f"[green]Added {len(file_paths)} files. Total relevant files: {len(RELEVANT_FILES)}[/green]" ) return f"{len(file_paths)} files added. Total relevant files: {len(RELEVANT_FILES)}" except Exception as e: console.log(f"[red]Error adding relevant files: {str(e)}[/red]") return f"Error: {str(e)}" def complete_task_output_relevant_files(reasoning: str) -> str: """Outputs the list of relevant files to a JSON file. Args: reasoning: Explanation of why we're outputting the files Returns: String indicating success or failure """ try: console.log( f"[blue]Complete Task Output Relevant Files Tool[/blue] - Reasoning: {reasoning}" ) global RELEVANT_FILES global OUTPUT_FILE if not RELEVANT_FILES: console.log(f"[yellow]No relevant files to output[/yellow]") return "No relevant files to output" # Write files to JSON with open(OUTPUT_FILE, "w") as f: json.dump(RELEVANT_FILES, f, indent=2) console.log( f"[green]Successfully wrote {len(RELEVANT_FILES)} files to {OUTPUT_FILE}[/green]" ) return f"Successfully wrote {len(RELEVANT_FILES)} files to {OUTPUT_FILE}" except Exception as e: console.log(f"[red]Error outputting relevant files: {str(e)}[/red]") return f"Error: {str(e)}" def display_token_usage(): """Displays the token usage and estimated cost.""" global INPUT_TOKENS, OUTPUT_TOKENS # Claude 3.7 Sonnet pricing (as of 25 February 2025) input_cost_per_million = 3.00 # $3.00 per million tokens output_cost_per_million = 15.00 # $15.00 per million tokens # Calculate costs input_cost = (INPUT_TOKENS / 1_000_000) * input_cost_per_million output_cost = (OUTPUT_TOKENS / 1_000_000) * output_cost_per_million total_cost = input_cost + output_cost # Create a nice table for display table = Table(title="Token Usage and Cost Summary") table.add_column("Category", style="cyan") table.add_column("Tokens", style="green") table.add_column("Rate", style="yellow") table.add_column("Cost", style="magenta") table.add_row( "Input", f"{INPUT_TOKENS:,}", f"${input_cost_per_million:.2f}/M", f"${input_cost:.4f}" ) table.add_row( "Output", f"{OUTPUT_TOKENS:,}", f"${output_cost_per_million:.2f}/M", f"${output_cost:.4f}" ) table.add_row( "Total", f"{INPUT_TOKENS + OUTPUT_TOKENS:,}", "", f"${total_cost:.4f}" ) console.print(Panel(table, title="Claude 3.7 Sonnet API Usage", subtitle="(Based on Feb 2025 pricing)")) return total_cost # Define tool schemas for Anthropic TOOLS = [ { "name": "git_list_files", "description": "Returns list of files in the repository, respecting gitignore", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why we need to list files relative to user request", }, "directory": { "type": "string", "description": "Directory to search in (defaults to current working directory)", }, "globs": { "type": "array", "items": {"type": "string"}, "description": "List of glob patterns to filter files (optional)", }, "extensions": { "type": "array", "items": {"type": "string"}, "description": "List of file extensions to filter files (optional)", }, }, "required": ["reasoning"], }, }, { "name": "check_file_paths_line_length", "description": "Checks the line length of each file and returns a dictionary of file paths and their line counts", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why we need to check line lengths", }, "file_paths": { "type": "array", "items": {"type": "string"}, "description": "List of file paths to check", }, }, "required": ["reasoning", "file_paths"], }, }, { "name": "determine_if_files_are_relevant", "description": "Determines if files are relevant to the prompt using parallelism", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why we need to determine relevance", }, "file_paths": { "type": "array", "items": {"type": "string"}, "description": "List of file paths to check", }, }, "required": ["reasoning", "file_paths"], }, }, { "name": "add_relevant_files", "description": "Adds files to the list of relevant files", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why we need to add these files", }, "file_paths": { "type": "array", "items": {"type": "string"}, "description": "List of file paths to add", }, }, "required": ["reasoning", "file_paths"], }, }, { "name": "complete_task_output_relevant_files", "description": "Outputs the list of relevant files to a JSON file. Call this when you have finished identifying all relevant files.", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why we are outputting the files to JSON", }, }, "required": ["reasoning"], }, }, ] AGENT_PROMPT = """ You are a codebase context builder. Use the available tools to search, filter and determine which files in the codebase are relevant to the prompt (user query). Start by listing files in the codebase using git_list_files, filtering by globs and extensions if provided. Check file line lengths to ensure they are within the specified limit using check_file_paths_line_length. Determine which files are relevant to the user query using determine_if_files_are_relevant. Add relevant files to the final list using add_relevant_files. Be thorough but efficient with tool usage. Think step by step about what information you need. Be sure to specify every parameter for each tool call. Every tool call should have a reasoning parameter which gives you a place to explain why you are calling the tool. The determine_if_files_are_relevant tool will process files in batches of 10 for efficiency. Focus on finding the most relevant files that will help answer the user query. You MUST monitor the number of files in the relevant files list. Once you have collected at least the File-Limit number of files, you MUST call complete_task_output_relevant_files to save the list of relevant files to JSON. If you've exhausted all potential relevant files before reaching the File-Limit, you should call complete_task_output_relevant_files with the files you have. Always end your work by calling complete_task_output_relevant_files, which outputs the list of relevant files to a JSON file. current-relevant-files is the current list of files that have been identified as relevant to your query. {{user_request}} Directory: {{directory}} Globs: {{globs}} Extensions: {{extensions}} File Line Limit: {{file_line_limit}} File-Limit: {{limit}} Output JSON: {{output_file}} {{relevant_files}} """ def main(): # Set up argument parser parser = argparse.ArgumentParser( description="Codebase Context Agent using Claude 3.7" ) parser.add_argument("-p", "--prompt", required=True, help="The user's request") parser.add_argument( "-d", "--directory", default=os.getcwd(), help="Directory to search in (defaults to current working directory)", ) parser.add_argument( "-g", "--globs", nargs="*", default=[], help="List of glob patterns to filter files (optional)", ) parser.add_argument( "-e", "--extensions", nargs="*", default=[], help="List of file extensions to filter files (optional)", ) parser.add_argument( "-q", "--quiet", action="store_true", help="Quiet mode (don't show logging)" ) parser.add_argument( "-l", "--limit", type=int, default=100, help="Maximum number of files to return" ) parser.add_argument( "-f", "--file-line-limit", type=int, default=500, help="Maximum number of lines per file", ) parser.add_argument( "-c", "--compute", type=int, default=10, help="Maximum number of agent loops (default: 10)", ) parser.add_argument( "-o", "--output-file", default="output_relevant_files.json", help="Path to output JSON file with relevant files (default: output_relevant_files.json)", ) args = parser.parse_args() # Configure the API key ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY") if not ANTHROPIC_API_KEY: console.print( "[red]Error: ANTHROPIC_API_KEY environment variable is not set[/red]" ) console.print( "Please get your API key from https://console.anthropic.com/settings/keys" ) console.print("Then set it with: export ANTHROPIC_API_KEY='your-api-key-here'") sys.exit(1) client = Anthropic(api_key=ANTHROPIC_API_KEY) # Set global variables global USER_PROMPT, OUTPUT_FILE USER_PROMPT = args.prompt OUTPUT_FILE = args.output_file # Configure quiet mode if args.quiet: console.quiet = True # For the first initialization, create the completed prompt # Will update this variable before each API call completed_prompt = ( AGENT_PROMPT.replace("{{user_request}}", args.prompt) .replace("{{directory}}", args.directory) .replace("{{globs}}", str(args.globs)) .replace("{{extensions}}", str(args.extensions)) .replace("{{file_line_limit}}", str(args.file_line_limit)) .replace("{{limit}}", str(args.limit)) .replace("{{output_file}}", OUTPUT_FILE) .replace("{{relevant_files}}", "No relevant files found yet.") ) # Initialize messages with proper typing for Anthropic chat messages = [{"role": "user", "content": completed_prompt}] compute_iterations = 0 break_loop = False # Main agent loop while True: if break_loop or compute_iterations >= args.compute: break console.rule( f"[yellow]Agent Loop {compute_iterations+1}/{args.compute}[/yellow]" ) compute_iterations += 1 try: # Before each API call, update the completed prompt with the current relevant files if RELEVANT_FILES: formatted_files = "\n".join([f"- {file}" for file in RELEVANT_FILES]) file_count = f"Total: {len(RELEVANT_FILES)}/{args.limit} files" relevant_files_section = f"{file_count}\n{formatted_files}" else: relevant_files_section = "No relevant files found yet." # Update the first message with the latest relevant files information completed_prompt = ( AGENT_PROMPT.replace("{{user_request}}", args.prompt) .replace("{{directory}}", args.directory) .replace("{{globs}}", str(args.globs)) .replace("{{extensions}}", str(args.extensions)) .replace("{{file_line_limit}}", str(args.file_line_limit)) .replace("{{limit}}", str(args.limit)) .replace("{{output_file}}", OUTPUT_FILE) .replace("{{relevant_files}}", relevant_files_section) ) # Always update the first message with the latest information before each API call messages[0]["content"] = completed_prompt # Generate content with tool support response = client.messages.create( model="claude-3-7-sonnet-20250219", system="You are a codebase context builder. Use the available tools to search, filter and determine which files in the codebase are relevant to the prompt (user query).", messages=messages, tools=TOOLS, max_tokens=4000, thinking={"type": "enabled", "budget_tokens": 2000}, ) # Track token usage global INPUT_TOKENS, OUTPUT_TOKENS if hasattr(response, 'usage') and response.usage: INPUT_TOKENS += response.usage.input_tokens OUTPUT_TOKENS += response.usage.output_tokens console.log(f"[dim]Token usage this call: {response.usage.input_tokens} input, {response.usage.output_tokens} output[/dim]") # Extract thinking block and other content thinking_block = None tool_use_block = None text_block = None if response.content: # Get the message content for content_block in response.content: if content_block.type == "thinking": thinking_block = content_block previous_thinking = thinking_block elif content_block.type == "tool_use": tool_use_block = content_block # Access the proper attributes directly tool_name = content_block.name tool_input = content_block.input tool_id = content_block.id elif content_block.type == "text": text_block = content_block console.print( f"[cyan]Model response:[/cyan] {content_block.text}" ) # Handle text responses if there was no tool use if not tool_use_block and text_block: messages.append( { # type: ignore "role": "assistant", "content": [ *([thinking_block] if thinking_block else []), {"type": "text", "text": text_block.text}, ], } ) break_loop = True continue # We need a tool use block to proceed if tool_use_block: console.print( f"[blue]Tool Call:[/blue] {tool_name}({json.dumps(tool_input, indent=2)})" ) try: # Execute the appropriate tool based on name if tool_name == "git_list_files": directory = tool_input.get("directory", args.directory) globs = tool_input.get("globs", args.globs) extensions = tool_input.get("extensions", args.extensions) result = git_list_files( reasoning=tool_input["reasoning"], directory=directory, globs=globs, extensions=extensions, ) elif tool_name == "check_file_paths_line_length": result = check_file_paths_line_length( reasoning=tool_input["reasoning"], file_paths=tool_input["file_paths"], file_line_limit=args.file_line_limit, ) elif tool_name == "determine_if_files_are_relevant": result = determine_if_files_are_relevant( reasoning=tool_input["reasoning"], file_paths=tool_input["file_paths"], ) elif tool_name == "add_relevant_files": result = add_relevant_files( reasoning=tool_input["reasoning"], file_paths=tool_input["file_paths"], ) elif tool_name == "complete_task_output_relevant_files": result = complete_task_output_relevant_files( reasoning=tool_input["reasoning"], ) # Indicate that we're done after writing the output break_loop = True else: raise Exception(f"Unknown tool call: {tool_name}") console.print( f"[blue]Tool Call Result:[/blue] {tool_name}(...) -> " ) console.print( Panel.fit( str(result), border_style="blue", ) ) # Append the tool result to messages messages.append( { # type: ignore "role": "assistant", "content": [ *([thinking_block] if thinking_block else []), { "type": "tool_use", "id": tool_id, "name": tool_name, "input": tool_input, }, ], } ) messages.append( { # type: ignore "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_id, "content": json.dumps(result), } ], } ) except Exception as e: error_msg = f"Error executing {tool_name}: {e}" console.print(f"[red]{error_msg}[/red]") # Append the error to messages messages.append( { # type: ignore "role": "assistant", "content": [ *([thinking_block] if thinking_block else []), { "type": "tool_use", "id": tool_id, "name": tool_name, "input": tool_input, }, ], } ) messages.append( { # type: ignore "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_id, "content": str(error_msg), } ], } ) # No need to update messages here since we're updating at the start of each loop iteration except Exception as e: console.print(f"[red]Error in agent loop: {str(e)}[/red]") raise e # Print the final list of relevant files console.rule("[green]Relevant Files[/green]") for i, file_path in enumerate(RELEVANT_FILES, 1): console.print(f"{i}. {file_path}") # Display token usage statistics console.rule("[yellow]Token Usage Summary[/yellow]") display_token_usage() if __name__ == "__main__": main() ================================================ FILE: sfa_codebase_context_agent_w_ripgrep_v3.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # "anthropic>=0.47.1", # "rich>=13.7.0", # "pydantic>=2.0.0", # ] # /// """ Usage: uv run sfa_codebase_context_agent_w_ripgrep_v3.py \ --prompt "Let's build a new metaprompt sfa agent using anthropic claude 3.7" \ --directory "." \ --globs "*.py" \ --extensions py md \ --limit 10 \ --file-line-limit 1000 \ --output-file relevant_files.json \ --compute 15 # Find files related to DuckDB implementations uv run sfa_codebase_context_agent_w_ripgrep_v3.py \ --prompt "Find all files related to DuckDB agent implementations" \ --file-line-limit 1000 \ --extensions py # Find all files related to Anthropic-powered agents uv run sfa_codebase_context_agent_w_ripgrep_v3.py \ --prompt "Identify all agents that use the new Claude 3.7 model" # Use ripgrep to search codebase for specific query uv run sfa_codebase_context_agent_w_ripgrep_v3.py \ --prompt "Find all files that use the Anthropic API" \ --use-ripgrep """ import os import sys import json import argparse import subprocess import time import fnmatch import concurrent.futures from typing import List, Dict, Any from rich.console import Console from anthropic import Anthropic from rich.table import Table from rich.panel import Panel # Initialize rich console console = Console() # Constants THINKING_BUDGET_TOKENS_PER_FILE = 2000 BATCH_SIZE = 10 MAX_RETRIES = 3 RETRY_WAIT = 1 # Global variables USER_PROMPT = "" RELEVANT_FILES = [] OUTPUT_FILE = "output_relevant_files.json" INPUT_TOKENS = 0 # To track input tokens to Anthropic API OUTPUT_TOKENS = 0 # To track output tokens from Anthropic API def git_list_files( reasoning: str, directory: str = os.getcwd(), globs: List[str] = [], extensions: List[str] = [], ) -> List[str]: """Returns a list of files in the repository, respecting gitignore. Args: reasoning: Explanation of why we're listing files directory: Directory to search in (defaults to current working directory) globs: List of glob patterns to filter files (optional) extensions: List of file extensions to filter files (optional) Returns: List of file paths as strings """ try: console.log(f"[blue]Git List Files Tool[/blue] - Reasoning: {reasoning}") console.log( f"[dim]Directory: {directory}, Globs: {globs}, Extensions: {extensions}[/dim]" ) # Change to the specified directory original_dir = os.getcwd() os.chdir(directory) # Get all files tracked by git result = subprocess.run( "git ls-files", shell=True, text=True, capture_output=True, ) files = result.stdout.strip().split("\n") # Filter by globs if provided if globs: filtered_files = [] for pattern in globs: for file in files: if fnmatch.fnmatch(file, pattern): filtered_files.append(file) files = filtered_files # Filter by extensions if provided if extensions: files = [ file for file in files if any(file.endswith(f".{ext}") for ext in extensions) ] # Change back to the original directory os.chdir(original_dir) # # Convert to absolute paths # files = [os.path.join(directory, file) for file in files] # Keep paths relative files = files console.log(f"[dim]Found {len(files)} files[/dim]") return files except Exception as e: console.log(f"[red]Error listing files: {str(e)}[/red]") return [] def check_file_paths_line_length( reasoning: str, file_paths: List[str], file_line_limit: int = 500 ) -> Dict[str, int]: """Checks the line length of each file and returns a dictionary of file paths and their line counts. Args: reasoning: Explanation of why we're checking line lengths file_paths: List of file paths to check file_line_limit: Maximum number of lines per file Returns: Dictionary mapping file paths to their total line counts """ try: console.log( f"[blue]Check File Paths Line Length Tool[/blue] - Reasoning: {reasoning}" ) console.log( f"[dim]Checking {len(file_paths)} files with line limit {file_line_limit}[/dim]" ) result = {} for file_path in file_paths: try: with open(file_path, "r", encoding="utf-8") as f: lines = f.readlines() line_count = len(lines) if line_count <= file_line_limit: result[file_path] = line_count else: console.log( f"[yellow]Skipping {file_path}: {line_count} lines exceed limit of {file_line_limit}[/yellow]" ) except Exception as e: console.log(f"[red]Error reading file {file_path}: {str(e)}[/red]") console.log(f"[dim]Found {len(result)} files within line limit[/dim]") return result except Exception as e: console.log(f"[red]Error checking file paths: {str(e)}[/red]") return {} def determine_if_file_is_relevant(prompt: str, file_path: str, client: Anthropic) -> Dict[str, Any]: # type: ignore """Determines if a single file is relevant to the prompt. Args: prompt: The user prompt file_path: Path to the file to check client: Anthropic client Returns: Dictionary with reasoning and is_relevant flag """ result = { "reasoning": "Error: Could not process file", "file_path": file_path, "is_relevant": False, } try: with open(file_path, "r", encoding="utf-8") as f: file_content = f.read() # Truncate file content if it's too long if len(file_content) > 10000: file_content = file_content[:10000] + "... [content truncated]" file_prompt = f""" You are a codebase context builder. Your task is to determine if a file is relevant to a user query. Analyze the file content and determine if it's relevant to the user query. Provide clear reasoning for your decision. Return a structured output with your reasoning and a boolean indicating relevance. Resond in JSON format following the json-output-format. {prompt} {file_path} {file_content} {{ "reasoning": "Explanation of why the file is relevant", "is_relevant": true | false }} """ for attempt in range(MAX_RETRIES): try: response = client.messages.create( model="claude-3-7-sonnet-20250219", max_tokens=3000, # Increased to be greater than thinking.budget_tokens thinking={ "type": "enabled", "budget_tokens": THINKING_BUDGET_TOKENS_PER_FILE, }, messages=[{"role": "user", "content": file_prompt}], system="Determine if the file is relevant to the user query.", ) # Track token usage global INPUT_TOKENS, OUTPUT_TOKENS if hasattr(response, 'usage') and response.usage: INPUT_TOKENS += response.usage.input_tokens OUTPUT_TOKENS += response.usage.output_tokens # Parse the response - look for text blocks response_text = None # Loop through all content blocks to find the text block for content_block in response.content: if content_block.type == "text": response_text = content_block.text break # Make sure we have a text response if response_text is None: raise Exception("No text response found in the model output") # Handle different response formats try: # Try parsing as JSON first result = json.loads(response_text) except json.JSONDecodeError: # If not valid JSON, try to extract reasoning and is_relevant from text is_relevant = "relevant" in response_text.lower() and not ( "not relevant" in response_text.lower() ) result = { "reasoning": response_text.strip(), "is_relevant": is_relevant, } return { "reasoning": result.get("reasoning", "No reasoning provided"), "file_path": file_path, "is_relevant": result.get("is_relevant", False), } except Exception as e: if attempt < MAX_RETRIES - 1: console.log( f"[yellow]Retry {attempt + 1}/{MAX_RETRIES} for {file_path}: {str(e)}[/yellow]" ) time.sleep(RETRY_WAIT) else: console.log( f"[red]Failed to determine relevance for {file_path}: {str(e)}[/red]" ) return { "reasoning": f"Error: {str(e)}", "file_path": file_path, "is_relevant": False, } except Exception as e: console.log(f"[red]Error processing file {file_path}: {str(e)}[/red]") return { "reasoning": f"Error: {str(e)}", "file_path": file_path, "is_relevant": False, } def determine_if_files_are_relevant( reasoning: str, file_paths: List[str] ) -> Dict[str, Any]: """Determines if files are relevant to the prompt using parallelism. Args: reasoning: Explanation of why we're determining relevance file_paths: List of file paths to check Returns: Dictionary with results for each file """ try: console.log( f"[blue]Determine If Files Are Relevant Tool[/blue] - Reasoning: {reasoning}" ) console.log( f"[dim]Checking {len(file_paths)} files in batches of {BATCH_SIZE}[/dim]" ) # Initialize Anthropic client client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY")) results = {} # Process files in batches for i in range(0, len(file_paths), BATCH_SIZE): batch = file_paths[i : i + BATCH_SIZE] console.log( f"[dim]Processing batch {i//BATCH_SIZE + 1}/{(len(file_paths) + BATCH_SIZE - 1)//BATCH_SIZE}[/dim]" ) # Process batch in parallel with concurrent.futures.ThreadPoolExecutor( max_workers=BATCH_SIZE ) as executor: future_to_file = { executor.submit( determine_if_file_is_relevant, USER_PROMPT, file_path, client ): file_path for file_path in batch } for future in concurrent.futures.as_completed(future_to_file): file_path = future_to_file[future] try: result = future.result() results[file_path] = result relevance = ( "Relevant" if result["is_relevant"] else "Not relevant" ) console.log(f"[dim]{file_path}: {relevance}[/dim]") except Exception as e: console.log( f"[red]Error processing {file_path}: {str(e)}[/red]" ) return results except Exception as e: console.log(f"[red]Error determining file relevance: {str(e)}[/red]") return {} def add_relevant_files(reasoning: str, file_paths: List[str]) -> str: """Adds files to the list of relevant files. Args: reasoning: Explanation of why we're adding these files file_paths: List of file paths to add Returns: String indicating success """ try: console.log(f"[blue]Add Relevant Files Tool[/blue] - Reasoning: {reasoning}") console.log(f"[dim]Adding {len(file_paths)} files to relevant files list[/dim]") global RELEVANT_FILES for file_path in file_paths: if file_path not in RELEVANT_FILES: RELEVANT_FILES.append(file_path) console.log( f"[green]Added {len(file_paths)} files. Total relevant files: {len(RELEVANT_FILES)}[/green]" ) return f"{len(file_paths)} files added. Total relevant files: {len(RELEVANT_FILES)}" except Exception as e: console.log(f"[red]Error adding relevant files: {str(e)}[/red]") return f"Error: {str(e)}" def complete_task_output_relevant_files(reasoning: str) -> str: """Outputs the list of relevant files to a JSON file. Args: reasoning: Explanation of why we're outputting the files Returns: String indicating success or failure """ try: console.log( f"[blue]Complete Task Output Relevant Files Tool[/blue] - Reasoning: {reasoning}" ) global RELEVANT_FILES global OUTPUT_FILE if not RELEVANT_FILES: console.log(f"[yellow]No relevant files to output[/yellow]") return "No relevant files to output" # Write files to JSON with open(OUTPUT_FILE, "w") as f: json.dump(RELEVANT_FILES, f, indent=2) console.log( f"[green]Successfully wrote {len(RELEVANT_FILES)} files to {OUTPUT_FILE}[/green]" ) return f"Successfully wrote {len(RELEVANT_FILES)} files to {OUTPUT_FILE}" except Exception as e: console.log(f"[red]Error outputting relevant files: {str(e)}[/red]") return f"Error: {str(e)}" def search_codebase_with_ripgrep( reasoning: str, query: str, base_path: str = ".", max_files: int = 10, extensions: List[str] = None, globs: List[str] = None ) -> Dict[str, Any]: """ Search the codebase at base_path for files relevant to the query using ripgrep. Args: reasoning: Explanation of why we're searching the codebase query: The search query base_path: Directory to search in (defaults to current working directory) max_files: Maximum number of top files to check (to limit processing) extensions: List of file extensions to filter files (e.g. ["py", "md"]) globs: List of glob patterns to filter files (e.g. ["*.py", "src/*.js"]) Returns: Dictionary with search results """ try: console.log(f"[blue]Ripgrep Search Tool[/blue] - Reasoning: {reasoning}") console.log(f"[dim]Searching for '{query}' in {base_path}[/dim]") # 1. Use ripgrep to find candidate files and match counts try: # Build ripgrep command with options # '-c' counts matches per file, '--no-config' to ignore custom ripgreprc rg_cmd = [ "rg", "-c", "--no-config", ] # Add extension filters if provided if extensions and len(extensions) > 0: for ext in extensions: rg_cmd.append(f"--type-add=custom:*.{ext}") rg_cmd.append("--type=custom") console.log(f"[dim]Filtering by extensions: {extensions}[/dim]") # Add glob patterns if provided if globs and len(globs) > 0: for glob in globs: rg_cmd.append(f"--glob={glob}") console.log(f"[dim]Filtering by globs: {globs}[/dim]") # Add the query and search path rg_cmd.append(query) rg_cmd.append(base_path) console.log(f"[dim]Running command: {' '.join(rg_cmd)}[/dim]") rg_result = subprocess.run(rg_cmd, capture_output=True, text=True) except Exception as e: raise RuntimeError(f"Failed to run ripgrep: {e}") output = rg_result.stdout.strip() candidates = [] if output: for line in output.splitlines(): # Each line is "filepath:count" parts = line.split(":", 1) if len(parts) == 2: file_path, count_str = parts[0], parts[1] else: # If ripgrep output format changes or there's a colon in filename, handle accordingly file_path = parts[0] count_str = "1" # Ensure the count is an integer try: count = int(count_str) except ValueError: count = 1 candidates.append((file_path, count)) else: # No matches found by ripgrep candidates = [] # Rank candidates by match count (descending) candidates.sort(key=lambda x: x[1], reverse=True) console.log(f"[dim]Found {len(candidates)} files matching query[/dim]") results = [] # Process top files up to max_files limit for idx, (file_path, count) in enumerate(candidates): if max_files is not None and idx >= max_files: break # Mark all files found by ripgrep as relevant since they contain the query result = {"file": file_path, "match_count": count, "relevant": True} results.append(result) # Add to our global relevant files list if file_path not in RELEVANT_FILES: RELEVANT_FILES.append(file_path) console.log(f"[green]Added {len(results)} files to relevant files list[/green]") return {"results": results, "total_matches": len(candidates)} except Exception as e: console.log(f"[red]Error searching with ripgrep: {str(e)}[/red]") return {"error": str(e), "results": [], "total_matches": 0} def display_token_usage(): """Displays the token usage and estimated cost.""" global INPUT_TOKENS, OUTPUT_TOKENS # Claude 3.7 Sonnet pricing (as of 25 February 2025) input_cost_per_million = 3.00 # $3.00 per million tokens output_cost_per_million = 15.00 # $15.00 per million tokens # Calculate costs input_cost = (INPUT_TOKENS / 1_000_000) * input_cost_per_million output_cost = (OUTPUT_TOKENS / 1_000_000) * output_cost_per_million total_cost = input_cost + output_cost # Create a nice table for display table = Table(title="Token Usage and Cost Summary") table.add_column("Category", style="cyan") table.add_column("Tokens", style="green") table.add_column("Rate", style="yellow") table.add_column("Cost", style="magenta") table.add_row( "Input", f"{INPUT_TOKENS:,}", f"${input_cost_per_million:.2f}/M", f"${input_cost:.4f}" ) table.add_row( "Output", f"{OUTPUT_TOKENS:,}", f"${output_cost_per_million:.2f}/M", f"${output_cost:.4f}" ) table.add_row( "Total", f"{INPUT_TOKENS + OUTPUT_TOKENS:,}", "", f"${total_cost:.4f}" ) console.print(Panel(table, title="Claude 3.7 Sonnet API Usage", subtitle="(Based on Feb 2025 pricing)")) return total_cost # Define tool schemas for Anthropic TOOLS = [ { "name": "search_codebase_with_ripgrep", "description": "Search the codebase for files that match a specific query using ripgrep. Fast and efficient for finding content.", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why we need to search the codebase", }, "query": { "type": "string", "description": "The search query to look for in file contents", }, "base_path": { "type": "string", "description": "Directory to search in (defaults to current working directory)", }, "max_files": { "type": "integer", "description": "Maximum number of top files to check (default: 10)", }, "extensions": { "type": "array", "items": {"type": "string"}, "description": "List of file extensions to filter by (e.g. ['py', 'md'])", }, "globs": { "type": "array", "items": {"type": "string"}, "description": "List of glob patterns to filter files (e.g. ['*.py', 'src/*.js'])", }, }, "required": ["reasoning", "query"], }, }, { "name": "git_list_files", "description": "Returns list of files in the repository, respecting gitignore", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why we need to list files relative to user request", }, "directory": { "type": "string", "description": "Directory to search in (defaults to current working directory)", }, "globs": { "type": "array", "items": {"type": "string"}, "description": "List of glob patterns to filter files (optional)", }, "extensions": { "type": "array", "items": {"type": "string"}, "description": "List of file extensions to filter files (optional)", }, }, "required": ["reasoning"], }, }, { "name": "check_file_paths_line_length", "description": "Checks the line length of each file and returns a dictionary of file paths and their line counts", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why we need to check line lengths", }, "file_paths": { "type": "array", "items": {"type": "string"}, "description": "List of file paths to check", }, }, "required": ["reasoning", "file_paths"], }, }, { "name": "determine_if_files_are_relevant", "description": "Determines if files are relevant to the prompt using parallelism", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why we need to determine relevance", }, "file_paths": { "type": "array", "items": {"type": "string"}, "description": "List of file paths to check", }, }, "required": ["reasoning", "file_paths"], }, }, { "name": "add_relevant_files", "description": "Adds files to the list of relevant files", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why we need to add these files", }, "file_paths": { "type": "array", "items": {"type": "string"}, "description": "List of file paths to add", }, }, "required": ["reasoning", "file_paths"], }, }, { "name": "complete_task_output_relevant_files", "description": "Outputs the list of relevant files to a JSON file. Call this when you have finished identifying all relevant files.", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why we are outputting the files to JSON", }, }, "required": ["reasoning"], }, }, ] AGENT_PROMPT = """ You are a codebase context builder. Use the available tools to search, filter and determine which files in the codebase are relevant to the prompt (user query). If ripgrep is enabled, use search_codebase_with_ripgrep to quickly find files containing specific content, which is faster and more precise for content searching. When using ripgrep, skip the determine_if_files_are_relevant tool as ripgrep already identifies relevant files. If ripgrep is not enabled, start by listing files in the codebase using git_list_files, filtering by globs and extensions if provided. Then check file line lengths and determine which files are relevant to the user query using determine_if_files_are_relevant. Check file line lengths to ensure they are within the specified limit using check_file_paths_line_length. Add relevant files to the final list using add_relevant_files if needed. Be thorough but efficient with tool usage. Think step by step about what information you need. Be sure to specify every parameter for each tool call. Every tool call should have a reasoning parameter which gives you a place to explain why you are calling the tool. The determine_if_files_are_relevant tool will process files in batches of 10 for efficiency (only use this if ripgrep is not enabled). Focus on finding the most relevant files that will help answer the user query. You MUST monitor the number of files in the relevant files list. Once you have collected at least the File-Limit number of files, you MUST call complete_task_output_relevant_files to save the list of relevant files to JSON. If you've exhausted all potential relevant files before reaching the File-Limit, you should call complete_task_output_relevant_files with the files you have. Always end your work by calling complete_task_output_relevant_files, which outputs the list of relevant files to a JSON file. current-relevant-files is the current list of files that have been identified as relevant to your query. {{user_request}} Directory: {{directory}} Globs: {{globs}} Extensions: {{extensions}} File Line Limit: {{file_line_limit}} File-Limit: {{limit}} Output JSON: {{output_file}} Use Ripgrep: {{use_ripgrep}} {{relevant_files}} """ def main(): # Set up argument parser parser = argparse.ArgumentParser( description="Codebase Context Agent using Claude 3.7" ) parser.add_argument("-p", "--prompt", required=True, help="The user's request") parser.add_argument( "-d", "--directory", default=os.getcwd(), help="Directory to search in (defaults to current working directory)", ) parser.add_argument( "-g", "--globs", nargs="*", default=[], help="List of glob patterns to filter files (optional)", ) parser.add_argument( "-e", "--extensions", nargs="*", default=[], help="List of file extensions to filter files (optional)", ) parser.add_argument( "-q", "--quiet", action="store_true", help="Quiet mode (don't show logging)" ) parser.add_argument( "-l", "--limit", type=int, default=100, help="Maximum number of files to return" ) parser.add_argument( "-f", "--file-line-limit", type=int, default=500, help="Maximum number of lines per file", ) parser.add_argument( "-c", "--compute", type=int, default=10, help="Maximum number of agent loops (default: 10)", ) parser.add_argument( "-o", "--output-file", default="output_relevant_files.json", help="Path to output JSON file with relevant files (default: output_relevant_files.json)", ) parser.add_argument( "--use-ripgrep", action="store_true", help="Use ripgrep to efficiently search file contents" ) parser.add_argument( "--max-ripgrep-files", type=int, default=10, help="Maximum number of files to return from ripgrep search" ) args = parser.parse_args() # Configure the API key ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY") if not ANTHROPIC_API_KEY: console.print( "[red]Error: ANTHROPIC_API_KEY environment variable is not set[/red]" ) console.print( "Please get your API key from https://console.anthropic.com/settings/keys" ) console.print("Then set it with: export ANTHROPIC_API_KEY='your-api-key-here'") sys.exit(1) client = Anthropic(api_key=ANTHROPIC_API_KEY) # Set global variables global USER_PROMPT, OUTPUT_FILE USER_PROMPT = args.prompt OUTPUT_FILE = args.output_file # Configure quiet mode if args.quiet: console.quiet = True # For the first initialization, create the completed prompt # Will update this variable before each API call completed_prompt = ( AGENT_PROMPT.replace("{{user_request}}", args.prompt) .replace("{{directory}}", args.directory) .replace("{{globs}}", str(args.globs)) .replace("{{extensions}}", str(args.extensions)) .replace("{{file_line_limit}}", str(args.file_line_limit)) .replace("{{limit}}", str(args.limit)) .replace("{{output_file}}", OUTPUT_FILE) .replace("{{use_ripgrep}}", str(args.use_ripgrep)) .replace("{{relevant_files}}", "No relevant files found yet.") ) # Initialize messages with proper typing for Anthropic chat messages = [{"role": "user", "content": completed_prompt}] compute_iterations = 0 break_loop = False # Main agent loop while True: if break_loop or compute_iterations >= args.compute: break console.rule( f"[yellow]Agent Loop {compute_iterations+1}/{args.compute}[/yellow]" ) compute_iterations += 1 try: # Before each API call, update the completed prompt with the current relevant files if RELEVANT_FILES: formatted_files = "\n".join([f"- {file}" for file in RELEVANT_FILES]) file_count = f"Total: {len(RELEVANT_FILES)}/{args.limit} files" relevant_files_section = f"{file_count}\n{formatted_files}" else: relevant_files_section = "No relevant files found yet." # Update the first message with the latest relevant files information completed_prompt = ( AGENT_PROMPT.replace("{{user_request}}", args.prompt) .replace("{{directory}}", args.directory) .replace("{{globs}}", str(args.globs)) .replace("{{extensions}}", str(args.extensions)) .replace("{{file_line_limit}}", str(args.file_line_limit)) .replace("{{limit}}", str(args.limit)) .replace("{{output_file}}", OUTPUT_FILE) .replace("{{use_ripgrep}}", str(args.use_ripgrep)) .replace("{{relevant_files}}", relevant_files_section) ) # Always update the first message with the latest information before each API call messages[0]["content"] = completed_prompt # Generate content with tool support response = client.messages.create( model="claude-3-7-sonnet-20250219", system="You are a codebase context builder. Use the available tools to search, filter and determine which files in the codebase are relevant to the prompt (user query).", messages=messages, tools=TOOLS, max_tokens=4000, thinking={"type": "enabled", "budget_tokens": 2000}, ) # Track token usage global INPUT_TOKENS, OUTPUT_TOKENS if hasattr(response, 'usage') and response.usage: INPUT_TOKENS += response.usage.input_tokens OUTPUT_TOKENS += response.usage.output_tokens console.log(f"[dim]Token usage this call: {response.usage.input_tokens} input, {response.usage.output_tokens} output[/dim]") # Extract thinking block and other content thinking_block = None tool_use_block = None text_block = None if response.content: # Get the message content for content_block in response.content: if content_block.type == "thinking": thinking_block = content_block previous_thinking = thinking_block elif content_block.type == "tool_use": tool_use_block = content_block # Access the proper attributes directly tool_name = content_block.name tool_input = content_block.input tool_id = content_block.id elif content_block.type == "text": text_block = content_block console.print( f"[cyan]Model response:[/cyan] {content_block.text}" ) # Handle text responses if there was no tool use if not tool_use_block and text_block: messages.append( { # type: ignore "role": "assistant", "content": [ *([thinking_block] if thinking_block else []), {"type": "text", "text": text_block.text}, ], } ) break_loop = True continue # We need a tool use block to proceed if tool_use_block: console.print( f"[blue]Tool Call:[/blue] {tool_name}({json.dumps(tool_input, indent=2)})" ) try: # Execute the appropriate tool based on name if tool_name == "git_list_files": directory = tool_input.get("directory", args.directory) globs = tool_input.get("globs", args.globs) extensions = tool_input.get("extensions", args.extensions) result = git_list_files( reasoning=tool_input["reasoning"], directory=directory, globs=globs, extensions=extensions, ) elif tool_name == "check_file_paths_line_length": result = check_file_paths_line_length( reasoning=tool_input["reasoning"], file_paths=tool_input["file_paths"], file_line_limit=args.file_line_limit, ) elif tool_name == "determine_if_files_are_relevant": result = determine_if_files_are_relevant( reasoning=tool_input["reasoning"], file_paths=tool_input["file_paths"], ) elif tool_name == "add_relevant_files": result = add_relevant_files( reasoning=tool_input["reasoning"], file_paths=tool_input["file_paths"], ) elif tool_name == "search_codebase_with_ripgrep": result = search_codebase_with_ripgrep( reasoning=tool_input["reasoning"], query=tool_input["query"], base_path=tool_input.get("base_path", args.directory), max_files=tool_input.get("max_files", args.max_ripgrep_files), extensions=tool_input.get("extensions", args.extensions), globs=tool_input.get("globs", args.globs), ) elif tool_name == "complete_task_output_relevant_files": result = complete_task_output_relevant_files( reasoning=tool_input["reasoning"], ) # Indicate that we're done after writing the output break_loop = True else: raise Exception(f"Unknown tool call: {tool_name}") console.print( f"[blue]Tool Call Result:[/blue] {tool_name}(...) -> " ) console.print( Panel.fit( str(result), border_style="blue", ) ) # Append the tool result to messages messages.append( { # type: ignore "role": "assistant", "content": [ *([thinking_block] if thinking_block else []), { "type": "tool_use", "id": tool_id, "name": tool_name, "input": tool_input, }, ], } ) messages.append( { # type: ignore "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_id, "content": json.dumps(result), } ], } ) except Exception as e: error_msg = f"Error executing {tool_name}: {e}" console.print(f"[red]{error_msg}[/red]") # Append the error to messages messages.append( { # type: ignore "role": "assistant", "content": [ *([thinking_block] if thinking_block else []), { "type": "tool_use", "id": tool_id, "name": tool_name, "input": tool_input, }, ], } ) messages.append( { # type: ignore "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_id, "content": str(error_msg), } ], } ) # No need to update messages here since we're updating at the start of each loop iteration except Exception as e: console.print(f"[red]Error in agent loop: {str(e)}[/red]") raise e # Print the final list of relevant files console.rule("[green]Relevant Files[/green]") for i, file_path in enumerate(RELEVANT_FILES, 1): console.print(f"{i}. {file_path}") # Display token usage statistics console.rule("[yellow]Token Usage Summary[/yellow]") display_token_usage() if __name__ == "__main__": main() ================================================ FILE: sfa_duckdb_anthropic_v2.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # "anthropic>=0.45.2", # "rich>=13.7.0", # ] # /// """ /// Example Usage # Run DuckDB agent with default compute loops (3) uv run sfa_duckdb_anthropic_v2.py -d ./data/analytics.db -p "Show me all users with score above 80" # Run with custom compute loops uv run sfa_duckdb_anthropic_v2.py -d ./data/analytics.db -p "Show me all users with score above 80" -c 5 /// """ import os import sys import json import argparse import subprocess from typing import List from rich.console import Console from rich.panel import Panel from anthropic import Anthropic # Initialize rich console console = Console() AGENT_PROMPT = """ You are a world-class expert at crafting precise DuckDB SQL queries. Your goal is to generate accurate queries that exactly match the user's data needs. Use the provided tools to explore the database and construct the perfect query. Start by listing tables to understand what's available. Describe tables to understand their schema and columns. Sample tables to see actual data patterns. Test queries before finalizing them. Only call run_final_sql_query when you're confident the query is perfect. Be thorough but efficient with tool usage. If you find your run_test_sql_query tool call returns an error or won't satisfy the user request, try to fix the query or try a different query. Think step by step about what information you need. Be sure to specify every parameter for each tool call. Every tool call should have a reasoning parameter which gives you a place to explain why you are calling the tool. list_tables Returns list of available tables in database reasoning string Why we need to list tables relative to user request true describe_table Returns schema info for specified table reasoning string Why we need to describe this table true table_name string Name of table to describe true sample_table Returns sample rows from specified table, always specify row_sample_size reasoning string Why we need to sample this table true table_name string Name of table to sample true row_sample_size integer Number of rows to sample aim for 3-5 rows true run_test_sql_query Tests a SQL query and returns results (only visible to agent) reasoning string Why we're testing this specific query true sql_query string The SQL query to test true run_final_sql_query Runs the final validated SQL query and shows results to user reasoning string Final explanation of how query satisfies user request true sql_query string The validated SQL query to run true {{user_request}} """ def list_tables(reasoning: str) -> List[str]: """Returns a list of tables in the database. The agent uses this to discover available tables and make informed decisions. Args: reasoning: Explanation of why we're listing tables relative to user request Returns: List of table names as strings """ try: result = subprocess.run( f'duckdb {DB_PATH} -c ".tables"', shell=True, text=True, capture_output=True, ) console.log(f"[blue]List Tables Tool[/blue] - Reasoning: {reasoning}") return result.stdout.strip().split("\n") except Exception as e: console.log(f"[red]Error listing tables: {str(e)}[/red]") return [] def describe_table(reasoning: str, table_name: str) -> str: """Returns schema information about the specified table. The agent uses this to understand table structure and available columns. Args: reasoning: Explanation of why we're describing this table table_name: Name of table to describe Returns: String containing table schema information """ try: result = subprocess.run( f'duckdb {DB_PATH} -c "DESCRIBE {table_name};"', shell=True, text=True, capture_output=True, ) console.log( f"[blue]Describe Table Tool[/blue] - Table: {table_name} - Reasoning: {reasoning}" ) return result.stdout except Exception as e: console.log(f"[red]Error describing table: {str(e)}[/red]") return "" def sample_table(reasoning: str, table_name: str, row_sample_size: int) -> str: """Returns a sample of rows from the specified table. The agent uses this to understand actual data content and patterns. Args: reasoning: Explanation of why we're sampling this table table_name: Name of table to sample from row_sample_size: Number of rows to sample aim for 3-5 rows Returns: String containing sample rows in readable format """ try: result = subprocess.run( f'duckdb {DB_PATH} -c "SELECT * FROM {table_name} LIMIT {row_sample_size};"', shell=True, text=True, capture_output=True, ) console.log( f"[blue]Sample Table Tool[/blue] - Table: {table_name} - Rows: {row_sample_size} - Reasoning: {reasoning}" ) return result.stdout except Exception as e: console.log(f"[red]Error sampling table: {str(e)}[/red]") return "" def run_test_sql_query(reasoning: str, sql_query: str) -> str: """Executes a test SQL query and returns results. The agent uses this to validate queries before finalizing them. Results are only shown to the agent, not the user. Args: reasoning: Explanation of why we're running this test query sql_query: The SQL query to test Returns: Query results as a string """ try: result = subprocess.run( f'duckdb {DB_PATH} -c "{sql_query}"', shell=True, text=True, capture_output=True, ) console.log(f"[blue]Test Query Tool[/blue] - Reasoning: {reasoning}") console.log(f"[dim]Query: {sql_query}[/dim]") return result.stdout except Exception as e: console.log(f"[red]Error running test query: {str(e)}[/red]") return str(e) def run_final_sql_query(reasoning: str, sql_query: str) -> str: """Executes the final SQL query and returns results to user. This is the last tool call the agent should make after validating the query. Args: reasoning: Final explanation of how this query satisfies user request sql_query: The validated SQL query to run Returns: Query results as a string """ try: result = subprocess.run( f'duckdb {DB_PATH} -c "{sql_query}"', shell=True, text=True, capture_output=True, ) console.log( Panel( f"[green]Final Query Tool[/green]\nReasoning: {reasoning}\nQuery: {sql_query}" ) ) return result.stdout except Exception as e: console.log(f"[red]Error running final query: {str(e)}[/red]") return str(e) def main(): # Set up argument parser parser = argparse.ArgumentParser(description="DuckDB Agent using Anthropic API") parser.add_argument( "-d", "--db", required=True, help="Path to DuckDB database file" ) parser.add_argument("-p", "--prompt", required=True, help="The user's request") parser.add_argument( "-c", "--compute", type=int, default=10, help="Maximum number of agent loops (default: 3)", ) args = parser.parse_args() # Configure the API key ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY") if not ANTHROPIC_API_KEY: console.print( "[red]Error: ANTHROPIC_API_KEY environment variable is not set[/red]" ) console.print("Please get your API key from your Anthropic dashboard") console.print("Then set it with: export ANTHROPIC_API_KEY='your-api-key-here'") sys.exit(1) # Set global DB_PATH for tool functions global DB_PATH DB_PATH = args.db # Initialize Anthropic client client = Anthropic() # Create a single combined prompt based on the full template completed_prompt = AGENT_PROMPT.replace("{{user_request}}", args.prompt) messages = [{"role": "user", "content": completed_prompt}] compute_iterations = 0 # Main agent loop while True: console.rule( f"[yellow]Agent Loop {compute_iterations+1}/{args.compute}[/yellow]" ) compute_iterations += 1 if compute_iterations >= args.compute: console.print( "[yellow]Warning: Reached maximum compute loops without final query[/yellow]" ) raise Exception( f"Maximum compute loops reached: {compute_iterations}/{args.compute}" ) try: # Add the user's initial prompt if this is the first iteration if compute_iterations == 1: messages.append({"role": "user", "content": args.prompt}) # Generate content with tool support response = client.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=1024, messages=messages, tools=[ { "name": "list_tables", "description": "Returns list of available tables in database", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Explanation for listing tables", } }, "required": ["reasoning"], }, }, { "name": "describe_table", "description": "Returns schema info for specified table", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why we need to describe this table", }, "table_name": { "type": "string", "description": "Name of table to describe", }, }, "required": ["reasoning", "table_name"], }, }, { "name": "sample_table", "description": "Returns sample rows from specified table", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why we need to sample this table", }, "table_name": { "type": "string", "description": "Name of table to sample", }, "row_sample_size": { "type": "integer", "description": "Number of rows to sample aim for 3-5 rows", }, }, "required": ["reasoning", "table_name", "row_sample_size"], }, }, { "name": "run_test_sql_query", "description": "Tests a SQL query and returns results (only visible to agent)", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why we're testing this specific query", }, "sql_query": { "type": "string", "description": "The SQL query to test", }, }, "required": ["reasoning", "sql_query"], }, }, { "name": "run_final_sql_query", "description": "Runs the final validated SQL query and shows results to user", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Final explanation of how query satisfies user request", }, "sql_query": { "type": "string", "description": "The validated SQL query to run", }, }, "required": ["reasoning", "sql_query"], }, }, ], tool_choice={"type": "any"}, # Always force a tool call ) # Look for tool calls in the response (expecting ToolUseBlock objects) tool_calls = [] for block in response.content: if hasattr(block, "type") and block.type == "tool_use": tool_calls.append(block) if tool_calls: for tool_call in tool_calls: tool_use_id = tool_call.id func_name = tool_call.name func_args = ( tool_call.input ) # already a dict; no need to call json.loads console.print( f"[blue]Tool Call:[/blue] {func_name}({json.dumps(func_args)})" ) messages.append({"role": "assistant", "content": response.content}) try: if func_name == "list_tables": result = list_tables(reasoning=func_args["reasoning"]) elif func_name == "describe_table": result = describe_table( reasoning=func_args["reasoning"], table_name=func_args["table_name"], ) elif func_name == "sample_table": result = sample_table( reasoning=func_args["reasoning"], table_name=func_args["table_name"], row_sample_size=func_args["row_sample_size"], ) elif func_name == "run_test_sql_query": result = run_test_sql_query( reasoning=func_args["reasoning"], sql_query=func_args["sql_query"], ) elif func_name == "run_final_sql_query": result = run_final_sql_query( reasoning=func_args["reasoning"], sql_query=func_args["sql_query"], ) console.print("\n[green]Final Results:[/green]") console.print(result) return else: raise Exception(f"Unknown tool call: {func_name}") console.print( f"[blue]Tool Call Result:[/blue] {func_name}(...) ->\n{result}" ) messages.append( { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_use_id, "content": str(result), } ], } ) except Exception as e: error_msg = f"Error executing {func_name}: {str(e)}" console.print(f"[red]{error_msg}[/red]") messages.append( { "role": "tool", "content": error_msg, "tool_call_id": tool_call.id, } ) continue else: raise Exception("No tool calls found in response - should never happen") except Exception as e: console.print(f"[red]Error in agent loop: {str(e)}[/red]") raise e if __name__ == "__main__": main() ================================================ FILE: sfa_duckdb_gemini_v1.py ================================================ #!/usr/bin/env python3 # /// script # dependencies = [ # "google-genai>=1.1.0", # ] # /// """ /// Example Usage # generates and executes DuckDB command (default) uv run sfa_duckdb_gemini_v1.py --db ./data/analytics.db "Filter employees with salary above 50000 and export to high_salary_employees.csv" # generates DuckDB command only without executing uv run sfa_duckdb_gemini_v1.py --db ./data/analytics.db --no-exe "Select name and department from employees table and save to employees.json" /// """ import os import sys import argparse import subprocess from google import genai DUCKDB_PROMPT = """ You are a world-class expert at crafting precise DuckDB CLI commands for database operations. Your goal is to generate accurate, minimal DuckDB commands that exactly match the user's data manipulation needs. Return ONLY the DuckDB command - no explanations, comments, or extra text. Create the command that satisfies the user query against the duckdb-database-path (e.g., mydb.db). Ensure the command follows DuckDB best practices for efficiency and readability. When the user requests to output results to a file, generate a command that writes to the specified file, or create a filename based on a shortened version of the user request and the input database name. If output is requested in CSV format, use the DuckDB COPY command with WITH (FORMAT CSV, HEADER, DELIMITER ','). If output is requested in JSON format, use the DuckDB COPY command with WITH (FORMAT JSON) to export results as JSON. When filtering or processing data, embed the query inside a COPY command if exporting, or run the query directly if no export is needed. Output your response by itself, do not use backticks or markdown formatting. We're going to run your response as a shell command immediately. If your results involve a table or query result set, default to exporting as a valid CSV or JSON file as requested. If the user request is to export to a file, ensure the file is created in the same directory as the duckdb-database-path unless specified otherwise. mydb.db Select the "name" and "age" columns from table employees where age > 30 duckdb mydb.db -c "SELECT name, age FROM employees WHERE age > 30;" data/order_data.db Filter records in table orders where total > 100 and export to orders_high.csv duckdb data/order_data.db -c "COPY (SELECT * FROM orders WHERE total > 100) TO 'orders_high.csv' WITH (FORMAT CSV, HEADER, DELIMITER ',');" analytics.db Convert table customers to JSON and save as customers.json duckdb analytics.db -c "COPY (SELECT * FROM customers) TO 'customers.json' WITH (FORMAT JSON);" mydb.db Export the result of a join between employees and departments from mydb.db to employees_departments.csv duckdb mydb.db -c "COPY (SELECT e.name, d.department FROM employees e JOIN departments d ON e.dept_id = d.id) TO 'employees_departments.csv' WITH (FORMAT CSV, HEADER, DELIMITER ',');" mydb.db Retrieve all records from table sales in mydb.db where region is 'North' duckdb mydb.db -c "SELECT * FROM sales WHERE region = 'North';" {{database_path}} {{user_request}} Your DuckDB command:""" def main(): # Set up argument parser parser = argparse.ArgumentParser( description="Generate DuckDB CLI command using Gemini API" ) parser.add_argument( "prompt", help="The DuckDB command request to send to Gemini", ) parser.add_argument( "--db", required=True, help="Path to DuckDB database file", ) parser.add_argument( "--no-exe", action="store_true", help="Generate the DuckDB command without executing it", ) args = parser.parse_args() # Configure the API key GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") if not GEMINI_API_KEY: print("Error: GEMINI_API_KEY environment variable is not set") print("Please get your API key from https://aistudio.google.com/app/apikey") print("Then set it with: export GEMINI_API_KEY='your-api-key-here'") sys.exit(1) # Initialize client client = genai.Client( api_key=GEMINI_API_KEY, http_options={"api_version": "v1alpha"} ) try: # Replace template variables in the prompt prompt = DUCKDB_PROMPT.replace("{{database_path}}", args.db) prompt = prompt.replace("{{user_request}}", args.prompt) # Generate DuckDB command response = client.models.generate_content( model="gemini-2.0-flash-001", contents=prompt ) duckdb_command = response.text.strip() print("\n🤖 Generated DuckDB command:", duckdb_command) # Execute the command unless --no-exe flag is present if not args.no_exe: print("\n🔍 Executing command...") # Execute the command using subprocess result = subprocess.run( duckdb_command, shell=True, text=True, capture_output=True ) if result.returncode != 0: print( f"\n❌ Error executing command (return code: {result.returncode}):", result.stderr, ) sys.exit(1) if result.stderr: print("❌ Error executing command:", result.stderr) if result.stdout: print("✅ Command executed successfully:") print(result.stdout) except Exception as e: print(f"\nError occurred: {str(e)}") sys.exit(1) if __name__ == "__main__": main() ================================================ FILE: sfa_duckdb_gemini_v2.py ================================================ #!/usr/bin/env python3 # /// script # dependencies = [ # "google-genai>=1.1.0", # "rich>=13.7.0", # ] # /// """ /// Example Usage # Run DuckDB agent with default compute loops (3) uv run sfa_duckdb_gemini_v2.py -d ./data/analytics.db -p "Show me all users with score above 80" # Run with custom compute loops uv run sfa_duckdb_gemini_v2.py -d ./data/analytics.db -p "Show me all users with score above 80" -c 5 /// """ import os import sys import json import argparse import subprocess from typing import List from rich.console import Console from rich.panel import Panel from google import genai from google.genai import types # Initialize rich console console = Console() def list_tables(reasoning: str) -> List[str]: """Returns a list of tables in the database. The agent uses this to discover available tables and make informed decisions. Args: reasoning: Explanation of why we're listing tables relative to user request Returns: List of table names as strings """ try: result = subprocess.run( f'duckdb {DB_PATH} -c ".tables"', # f"duckdb {DB_PATH} -c \"SELECT name FROM sqlite_master WHERE type='table';\"", shell=True, text=True, capture_output=True, ) console.log(f"[blue]List Tables Tool[/blue] - Reasoning: {reasoning}") return result.stdout.strip().split("\n") except Exception as e: console.log(f"[red]Error listing tables: {str(e)}[/red]") return [] def describe_table(reasoning: str, table_name: str) -> str: """Returns schema information about the specified table. The agent uses this to understand table structure and available columns. Args: reasoning: Explanation of why we're describing this table table_name: Name of table to describe Returns: String containing table schema information """ try: result = subprocess.run( f'duckdb {DB_PATH} -c "DESCRIBE {table_name};"', shell=True, text=True, capture_output=True, ) console.log( f"[blue]Describe Table Tool[/blue] - Table: {table_name} - Reasoning: {reasoning}" ) return result.stdout except Exception as e: console.log(f"[red]Error describing table: {str(e)}[/red]") return "" def sample_table(reasoning: str, table_name: str, row_sample_size: int) -> str: """Returns a sample of rows from the specified table. The agent uses this to understand actual data content and patterns. Args: reasoning: Explanation of why we're sampling this table table_name: Name of table to sample from row_sample_size: Number of rows to sample aim for 3-5 rows Returns: String containing sample rows in readable format """ try: result = subprocess.run( f'duckdb {DB_PATH} -c "SELECT * FROM {table_name} LIMIT {row_sample_size};"', shell=True, text=True, capture_output=True, ) console.log( f"[blue]Sample Table Tool[/blue] - Table: {table_name} - Rows: {row_sample_size} - Reasoning: {reasoning}" ) return result.stdout except Exception as e: console.log(f"[red]Error sampling table: {str(e)}[/red]") return "" def run_test_sql_query(reasoning: str, sql_query: str) -> str: """Executes a test SQL query and returns results. The agent uses this to validate queries before finalizing them. Results are only shown to the agent, not the user. Args: reasoning: Explanation of why we're running this test query sql_query: The SQL query to test Returns: Query results as a string """ try: result = subprocess.run( f'duckdb {DB_PATH} -c "{sql_query}"', shell=True, text=True, capture_output=True, ) console.log(f"[blue]Test Query Tool[/blue] - Reasoning: {reasoning}") console.log(f"[dim]Query: {sql_query}[/dim]") return result.stdout except Exception as e: console.log(f"[red]Error running test query: {str(e)}[/red]") return str(e) def run_final_sql_query(reasoning: str, sql_query: str) -> str: """Executes the final SQL query and returns results to user. This is the last tool call the agent should make after validating the query. Args: reasoning: Final explanation of how this query satisfies user request sql_query: The validated SQL query to run Returns: Query results as a string """ try: result = subprocess.run( f'duckdb {DB_PATH} -c "{sql_query}"', shell=True, text=True, capture_output=True, ) console.log( Panel( f"[green]Final Query Tool[/green]\nReasoning: {reasoning}\nQuery: {sql_query}" ) ) return result.stdout except Exception as e: console.log(f"[red]Error running final query: {str(e)}[/red]") return str(e) AGENT_PROMPT = """ You are a world-class expert at crafting precise DuckDB SQL queries. Your goal is to generate accurate queries that exactly match the user's data needs. Use the provided tools to explore the database and construct the perfect query. Start by listing tables to understand what's available. Describe tables to understand their schema and columns. Sample tables to see actual data patterns. Test queries before finalizing them. Only call run_final_sql_query when you're confident the query is perfect. Be thorough but efficient with tool usage. If you find your run_test_sql_query tool call returns an error or won't satisfy the user request, try to fix the query or try a different query. Think step by step about what information you need. Be sure to specify every parameter for each tool call. Every tool call should have a reasoning parameter which gives you a place to explain why you are calling the tool. list_tables Returns list of available tables in database reasoning string Why we need to list tables relative to user request true describe_table Returns schema info for specified table reasoning string Why we need to describe this table true table_name string Name of table to describe true sample_table Returns sample rows from specified table, always specify row_sample_size reasoning string Why we need to sample this table true table_name string Name of table to sample true row_sample_size integer Number of rows to sample aim for 3-5 rows true run_test_sql_query Tests a SQL query and returns results (only visible to agent) reasoning string Why we're testing this specific query true sql_query string The SQL query to test true run_final_sql_query Runs the final validated SQL query and shows results to user reasoning string Final explanation of how query satisfies user request true sql_query string The validated SQL query to run true {{user_request}} """ def main(): # Set up argument parser parser = argparse.ArgumentParser(description="DuckDB Agent using Gemini API") parser.add_argument( "-d", "--db", required=True, help="Path to DuckDB database file" ) parser.add_argument("-p", "--prompt", required=True, help="The user's request") parser.add_argument( "-c", "--compute", type=int, default=10, help="Maximum number of agent loops (default: 3)", ) args = parser.parse_args() # Configure the API key GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") if not GEMINI_API_KEY: console.print( "[red]Error: GEMINI_API_KEY environment variable is not set[/red]" ) console.print( "Please get your API key from https://aistudio.google.com/app/apikey" ) console.print("Then set it with: export GEMINI_API_KEY='your-api-key-here'") sys.exit(1) # Set global DB_PATH for tool functions global DB_PATH DB_PATH = args.db # Initialize Gemini client client = genai.Client(api_key=GEMINI_API_KEY) completed_prompt = AGENT_PROMPT.replace("{{user_request}}", args.prompt) # Initialize message history with proper Content type messages = [ types.Content(role="user", parts=[types.Part.from_text(text=completed_prompt)]) ] compute_iterations = 0 # Main agent loop while True: console.rule( f"[yellow]Agent Loop {compute_iterations+1}/{args.compute}[/yellow]" ) compute_iterations += 1 if compute_iterations >= args.compute: console.print( "[yellow]Warning: Reached maximum compute loops without final query[/yellow]" ) raise Exception( f"Maximum compute loops reached: {compute_iterations}/{args.compute}" ) try: # Generate content with tool support response = client.models.generate_content( model="gemini-2.0-flash-001", # model="gemini-1.5-flash", contents=[ *messages, ], config=types.GenerateContentConfig( tools=[ list_tables, describe_table, sample_table, run_test_sql_query, run_final_sql_query, ], automatic_function_calling=types.AutomaticFunctionCallingConfig( # maximum_remote_calls=2 # disable=True ), tool_config=types.ToolConfig( function_calling_config=types.FunctionCallingConfig(mode="ANY") ), ), ) # Process tool calls if response.function_calls: for func_call in response.function_calls: # Extract function name and args func_name = func_call.name func_args = func_call.args console.print( f"[blue]Function Call:[/blue] {func_name}({func_args})" ) try: # Call appropriate function if func_name == "list_tables": result = list_tables(**func_args) elif func_name == "describe_table": result = describe_table(**func_args) elif func_name == "sample_table": result = sample_table(**func_args) elif func_name == "run_test_sql_query": result = run_test_sql_query(**func_args) elif func_name == "run_final_sql_query": result = run_final_sql_query(**func_args) console.print("\n[green]Final Results:[/green]") console.print(result) return # Exit after final query console.print( f"[blue]Function Call Result:[/blue] {func_name}(...) ->\n{result}" ) # Add function response as proper Content type function_response = {"result": str(result)} function_response_part = types.Part.from_function_response( name=func_name, response=function_response, ) # Add model's function call as Content messages.append(response.candidates[0].content) messages.append( types.Content(role="tool", parts=[function_response_part]) ) except Exception as e: # Add error response as proper Content type error_msg = f"Error executing {func_name}: {str(e)}" function_response = {"error": error_msg} function_response_part = types.Part.from_function_response( name=func_name, response=function_response, ) messages.append(response.candidates[0].content) messages.append( types.Content(role="tool", parts=[function_response_part]) ) console.print(f"[red]{error_msg}[/red]") continue else: # Add model response as proper Content type messages.append(response.candidates[0].content) except Exception as e: console.print(f"[red]Error in agent loop: {str(e)}[/red]") raise e if __name__ == "__main__": main() ================================================ FILE: sfa_duckdb_openai_v2.py ================================================ # /// script # dependencies = [ # "openai>=1.63.0", # "rich>=13.7.0", # "pydantic>=2.0.0", # ] # /// import os import sys import json import argparse import subprocess from typing import List from rich.console import Console from rich.panel import Panel import openai from pydantic import BaseModel, Field, ValidationError from openai import pydantic_function_tool # Initialize rich console console = Console() # Create our list of function tools from our pydantic models class ListTablesArgs(BaseModel): reasoning: str = Field( ..., description="Explanation for listing tables relative to the user request" ) class DescribeTableArgs(BaseModel): reasoning: str = Field(..., description="Reason why the table schema is needed") table_name: str = Field(..., description="Name of the table to describe") class SampleTableArgs(BaseModel): reasoning: str = Field(..., description="Explanation for sampling the table") table_name: str = Field(..., description="Name of the table to sample") row_sample_size: int = Field( ..., description="Number of rows to sample (aim for 3-5 rows)" ) class RunTestSQLQuery(BaseModel): reasoning: str = Field(..., description="Reason for testing this query") sql_query: str = Field(..., description="The SQL query to test") class RunFinalSQLQuery(BaseModel): reasoning: str = Field( ..., description="Final explanation of how this query satisfies the user request", ) sql_query: str = Field(..., description="The validated SQL query to run") # Create tools list tools = [ pydantic_function_tool(ListTablesArgs), pydantic_function_tool(DescribeTableArgs), pydantic_function_tool(SampleTableArgs), pydantic_function_tool(RunTestSQLQuery), pydantic_function_tool(RunFinalSQLQuery), ] AGENT_PROMPT = """ You are a world-class expert at crafting precise DuckDB SQL queries. Your goal is to generate accurate queries that exactly match the user's data needs. Use the provided tools to explore the database and construct the perfect query. Start by listing tables to understand what's available. Describe tables to understand their schema and columns. Sample tables to see actual data patterns. Test queries before finalizing them. Only call run_final_sql_query when you're confident the query is perfect. Be thorough but efficient with tool usage. If you find your run_test_sql_query tool call returns an error or won't satisfy the user request, try to fix the query or try a different query. Think step by step about what information you need. Be sure to specify every parameter for each tool call. Every tool call should have a reasoning parameter which gives you a place to explain why you are calling the tool. list_tables Returns list of available tables in database reasoning string Why we need to list tables relative to user request true describe_table Returns schema info for specified table reasoning string Why we need to describe this table true table_name string Name of table to describe true sample_table Returns sample rows from specified table, always specify row_sample_size reasoning string Why we need to sample this table true table_name string Name of table to sample true row_sample_size integer Number of rows to sample aim for 3-5 rows true run_test_sql_query Tests a SQL query and returns results (only visible to agent) reasoning string Why we're testing this specific query true sql_query string The SQL query to test true run_final_sql_query Runs the final validated SQL query and shows results to user reasoning string Final explanation of how query satisfies user request true sql_query string The validated SQL query to run true {{user_request}} """ def list_tables(reasoning: str) -> List[str]: """Returns a list of tables in the database. The agent uses this to discover available tables and make informed decisions. Args: reasoning: Explanation of why we're listing tables relative to user request Returns: List of table names as strings """ try: result = subprocess.run( f'duckdb {DB_PATH} -c ".tables"', shell=True, text=True, capture_output=True, ) console.log(f"[blue]List Tables Tool[/blue] - Reasoning: {reasoning}") return result.stdout.strip().split("\n") except Exception as e: console.log(f"[red]Error listing tables: {str(e)}[/red]") return [] def describe_table(reasoning: str, table_name: str) -> str: """Returns schema information about the specified table. The agent uses this to understand table structure and available columns. Args: reasoning: Explanation of why we're describing this table table_name: Name of table to describe Returns: String containing table schema information """ try: result = subprocess.run( f'duckdb {DB_PATH} -c "DESCRIBE {table_name};"', shell=True, text=True, capture_output=True, ) console.log( f"[blue]Describe Table Tool[/blue] - Table: {table_name} - Reasoning: {reasoning}" ) return result.stdout except Exception as e: console.log(f"[red]Error describing table: {str(e)}[/red]") return "" def sample_table(reasoning: str, table_name: str, row_sample_size: int) -> str: """Returns a sample of rows from the specified table. The agent uses this to understand actual data content and patterns. Args: reasoning: Explanation of why we're sampling this table table_name: Name of table to sample from row_sample_size: Number of rows to sample aim for 3-5 rows Returns: String containing sample rows in readable format """ try: result = subprocess.run( f'duckdb {DB_PATH} -c "SELECT * FROM {table_name} LIMIT {row_sample_size};"', shell=True, text=True, capture_output=True, ) console.log( f"[blue]Sample Table Tool[/blue] - Table: {table_name} - Rows: {row_sample_size} - Reasoning: {reasoning}" ) return result.stdout except Exception as e: console.log(f"[red]Error sampling table: {str(e)}[/red]") return "" def run_test_sql_query(reasoning: str, sql_query: str) -> str: """Executes a test SQL query and returns results. The agent uses this to validate queries before finalizing them. Results are only shown to the agent, not the user. Args: reasoning: Explanation of why we're running this test query sql_query: The SQL query to test Returns: Query results as a string """ try: result = subprocess.run( f'duckdb {DB_PATH} -c "{sql_query}"', shell=True, text=True, capture_output=True, ) console.log(f"[blue]Test Query Tool[/blue] - Reasoning: {reasoning}") console.log(f"[dim]Query: {sql_query}[/dim]") return result.stdout except Exception as e: console.log(f"[red]Error running test query: {str(e)}[/red]") return str(e) def run_final_sql_query(reasoning: str, sql_query: str) -> str: """Executes the final SQL query and returns results to user. This is the last tool call the agent should make after validating the query. Args: reasoning: Final explanation of how this query satisfies user request sql_query: The validated SQL query to run Returns: Query results as a string """ try: result = subprocess.run( f'duckdb {DB_PATH} -c "{sql_query}"', shell=True, text=True, capture_output=True, ) console.log( Panel( f"[green]Final Query Tool[/green]\nReasoning: {reasoning}\nQuery: {sql_query}" ) ) return result.stdout except Exception as e: console.log(f"[red]Error running final query: {str(e)}[/red]") return str(e) def main(): # Set up argument parser parser = argparse.ArgumentParser(description="DuckDB Agent using OpenAI API") parser.add_argument( "-d", "--db", required=True, help="Path to DuckDB database file" ) parser.add_argument("-p", "--prompt", required=True, help="The user's request") parser.add_argument( "-c", "--compute", type=int, default=10, help="Maximum number of agent loops (default: 3)", ) args = parser.parse_args() # Configure the API key OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") if not OPENAI_API_KEY: console.print( "[red]Error: OPENAI_API_KEY environment variable is not set[/red]" ) console.print( "Please get your API key from https://platform.openai.com/api-keys" ) console.print("Then set it with: export OPENAI_API_KEY='your-api-key-here'") sys.exit(1) openai.api_key = OPENAI_API_KEY # Set global DB_PATH for tool functions global DB_PATH DB_PATH = args.db # Create a single combined prompt based on the full template completed_prompt = AGENT_PROMPT.replace("{{user_request}}", args.prompt) messages = [{"role": "user", "content": completed_prompt}] compute_iterations = 0 # Main agent loop while True: console.rule( f"[yellow]Agent Loop {compute_iterations+1}/{args.compute}[/yellow]" ) compute_iterations += 1 if compute_iterations >= args.compute: console.print( "[yellow]Warning: Reached maximum compute loops without final query[/yellow]" ) raise Exception( f"Maximum compute loops reached: {compute_iterations}/{args.compute}" ) try: # Generate content with tool support response = openai.chat.completions.create( model="o3-mini", # model="gpt-4o-mini", messages=messages, tools=tools, tool_choice="required", ) if response.choices: assert len(response.choices) == 1 message = response.choices[0].message if message.function_call: func_call = message.function_call elif message.tool_calls and len(message.tool_calls) > 0: # If a tool_calls list is present, use the first call and extract its function details. tool_call = message.tool_calls[0] func_call = tool_call.function else: func_call = None if func_call: func_name = func_call.name func_args_str = func_call.arguments messages.append( { # type: ignore "role": "assistant", "tool_calls": [ { "id": tool_call.id, "type": "function", "function": func_call, } ], } ) console.print( f"[blue]Function Call:[/blue] {func_name}({func_args_str})" ) try: # Validate and parse arguments using the corresponding pydantic model if func_name == "ListTablesArgs": args_parsed = ListTablesArgs.model_validate_json( func_args_str ) result = list_tables(reasoning=args_parsed.reasoning) elif func_name == "DescribeTableArgs": args_parsed = DescribeTableArgs.model_validate_json( func_args_str ) result = describe_table( reasoning=args_parsed.reasoning, table_name=args_parsed.table_name, ) elif func_name == "SampleTableArgs": args_parsed = SampleTableArgs.model_validate_json( func_args_str ) result = sample_table( reasoning=args_parsed.reasoning, table_name=args_parsed.table_name, row_sample_size=args_parsed.row_sample_size, ) elif func_name == "RunTestSQLQuery": args_parsed = RunTestSQLQuery.model_validate_json( func_args_str ) result = run_test_sql_query( reasoning=args_parsed.reasoning, sql_query=args_parsed.sql_query, ) elif func_name == "RunFinalSQLQuery": args_parsed = RunFinalSQLQuery.model_validate_json( func_args_str ) result = run_final_sql_query( reasoning=args_parsed.reasoning, sql_query=args_parsed.sql_query, ) console.print("\n[green]Final Results:[/green]") console.print(result) return else: raise Exception(f"Unknown tool call: {func_name}") console.print( f"[blue]Function Call Result:[/blue] {func_name}(...) ->\n{result}" ) # Append the function call result into our messages as a tool response messages.append( { "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps({"result": str(result)}), } ) except Exception as e: error_msg = f"Argument validation failed for {func_name}: {e}" console.print(f"[red]{error_msg}[/red]") messages.append( { "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps({"error": error_msg}), } ) continue else: raise Exception( "No function call in this response - should never happen" ) except Exception as e: console.print(f"[red]Error in agent loop: {str(e)}[/red]") raise e if __name__ == "__main__": main() ================================================ FILE: sfa_file_editor_sonny37_v1.py ================================================ #!/usr/bin/env python3 # /// script # dependencies = [ # "anthropic>=0.49.0", # "rich>=13.7.0", # ] # /// """ /// Example Usage # View a file uv run sfa_file_editor_sonny37_v1.py --prompt "Show me the content of README.md" # Use token-efficient tools (reduces token usage by ~14% on average) uv run sfa_file_editor_sonny37_v1.py --prompt "Read the first 20 lines of content from README.md and summarize into a new README_SUMMARY.md" --efficiency # Edit a file uv run sfa_file_editor_sonny37_v1.py --prompt "Fix the syntax error in sfa_poc.py" # Create a new file uv run sfa_file_editor_sonny37_v1.py --prompt "Create a new file called hello.py with a function that prints Hello World" # Add docstrings to functions uv run sfa_file_editor_sonny37_v1.py --prompt "Add proper docstrings to all functions in sfa_poc.py" # Insert code at specific location uv run sfa_file_editor_sonny37_v1.py --prompt "Insert error handling code before the API call in sfa_duckdb_openai_v2.py" # Modify multiple files uv run sfa_file_editor_sonny37_v1.py --prompt "Update all print statements in agent_workspace directory to use f-strings" # Refactor code uv run sfa_file_editor_sonny37_v1.py --prompt "Refactor the factorial function in agent_workspace/test.py to use iteration instead of recursion" # Create new test files uv run sfa_file_editor_sonny37_v1.py --prompt "Create unit tests for the functions in sfa_file_editor_sonny37_v1.py and save them in agent_workspace/test_file_editor.py" # Run with higher thinking tokens uv run sfa_file_editor_sonny37_v1.py --prompt "Refactor README.md to make it more concise" --thinking 5000 # Increase max loops for complex tasks uv run sfa_file_editor_sonny37_v1.py --prompt "Create a Python class that implements a binary search tree with insert, delete, and search methods" --max-loops 20 # Combine multiple flags uv run sfa_file_editor_sonny37_v1.py --prompt "Create a Flask API with 3 endpoints inside of agent_workspace/api_server.py" --thinking 6000 --max-loops 25 uv run sfa_file_editor_sonny37_v1.py --prompt "Create a Flask API with 3 endpoints inside of agent_workspace/api_server.py" --efficiency --thinking 6000 --max-loops 25 /// """ import os import sys import argparse import time import json import traceback from typing import List, Dict, Any, Optional, Tuple, Union from rich.console import Console from rich.panel import Panel from rich.markdown import Markdown from rich.syntax import Syntax from rich.table import Table from rich.style import Style from rich.align import Align from anthropic import Anthropic # Initialize rich console console = Console() # Define constants MODEL = "claude-3-7-sonnet-20250219" DEFAULT_THINKING_TOKENS = 3000 def display_token_usage(input_tokens: int, output_tokens: int) -> None: """ Display token usage information in a rich formatted table Args: input_tokens: Number of input tokens used output_tokens: Number of output tokens used """ total_tokens = input_tokens + output_tokens token_ratio = output_tokens / input_tokens if input_tokens > 0 else 0 # Create a table for token usage table = Table(title="Token Usage Statistics", expand=True) # Add columns with proper styling table.add_column("Metric", style="cyan", no_wrap=True) table.add_column("Count", style="magenta", justify="right") table.add_column("Percentage", justify="right") # Add rows with data table.add_row( "Input Tokens", f"{input_tokens:,}", f"{input_tokens/total_tokens:.1%}" ) table.add_row( "Output Tokens", f"{output_tokens:,}", f"{output_tokens/total_tokens:.1%}" ) table.add_row("Total Tokens", f"{total_tokens:,}", "100.0%") table.add_row("Output/Input Ratio", f"{token_ratio:.2f}", "") console.print() console.print(table) def normalize_path(path: str) -> str: """ Normalize file paths to handle various formats (absolute, relative, Windows paths, etc.) Args: path: The path to normalize Returns: The normalized path """ if not path: return path # Handle Windows backslash paths if provided path = path.replace("\\", os.sep) is_windows_path = False if os.name == "nt" and len(path) > 1 and path[1] == ":": is_windows_path = True # Handle /repo/ paths from Claude (tool use convention) if path.startswith("/repo/"): path = os.path.join(os.getcwd(), path[6:]) return path if path.startswith("/"): # Handle case when Claude provides paths with leading slash if path == "/" or path == "/.": # Special case for root directory path = os.getcwd() else: # Replace leading slash with current working directory path = os.path.join(os.getcwd(), path[1:]) elif path.startswith("./"): # Handle relative paths starting with ./ path = os.path.join(os.getcwd(), path[2:]) elif not os.path.isabs(path) and not is_windows_path: # For non-absolute paths that aren't Windows paths either path = os.path.join(os.getcwd(), path) return path def view_file(path: str, view_range=None) -> Dict[str, Any]: """ View the contents of a file. Args: path: The path to the file to view view_range: Optional start and end lines to view [start, end] Returns: Dictionary with content or error message """ try: # Normalize the path path = normalize_path(path) if not os.path.exists(path): error_msg = f"File {path} does not exist" console.log(f"[view_file] Error: {error_msg}") return {"error": error_msg} with open(path, "r") as f: lines = f.readlines() if view_range: start, end = view_range # Convert to 0-indexed for Python start = max(0, start - 1) if end == -1: end = len(lines) else: end = min(len(lines), end) lines = lines[start:end] content = "".join(lines) # Display the file content (only for console, not returned to Claude) file_extension = os.path.splitext(path)[1][1:] # Get extension without the dot syntax = Syntax(content, file_extension or "text", line_numbers=True) console.print(Panel(syntax, title=f"File: {path}")) return {"result": content} except Exception as e: error_msg = f"Error viewing file: {str(e)}" console.print(f"[red]{error_msg}[/red]") console.log(f"[view_file] Error: {str(e)}") console.log(traceback.format_exc()) return {"error": error_msg} def str_replace(path: str, old_str: str, new_str: str) -> Dict[str, Any]: """ Replace a specific string in a file. Args: path: The path to the file to modify old_str: The text to replace new_str: The new text to insert Returns: Dictionary with result or error message """ try: # Normalize the path path = normalize_path(path) if not os.path.exists(path): error_msg = f"File {path} does not exist" console.log(f"[str_replace] Error: {error_msg}") return {"error": error_msg} with open(path, "r") as f: content = f.read() if old_str not in content: error_msg = f"The specified string was not found in the file {path}" console.log(f"[str_replace] Error: {error_msg}") return {"error": error_msg} new_content = content.replace(old_str, new_str, 1) with open(path, "w") as f: f.write(new_content) console.print(f"[green]Successfully replaced text in {path}[/green]") console.log(f"[str_replace] Successfully replaced text in {path}") return {"result": f"Successfully replaced text in {path}"} except Exception as e: error_msg = f"Error replacing text: {str(e)}" console.print(f"[red]{error_msg}[/red]") console.log(f"[str_replace] Error: {str(e)}") console.log(traceback.format_exc()) return {"error": error_msg} def create_file(path: str, file_text: str) -> Dict[str, Any]: """ Create a new file with specified content. Args: path: The path where the new file should be created file_text: The content to write to the new file Returns: Dictionary with result or error message """ try: # Check if the path is empty or invalid if not path or not path.strip(): error_msg = "Invalid file path provided: path is empty." console.log(f"[create_file] Error: {error_msg}") return {"error": error_msg} # Normalize the path path = normalize_path(path) # Check if the directory exists directory = os.path.dirname(path) if directory and not os.path.exists(directory): console.log(f"[create_file] Creating directory: {directory}") os.makedirs(directory) with open(path, "w") as f: f.write(file_text or "") console.print(f"[green]Successfully created file {path}[/green]") console.log(f"[create_file] Successfully created file {path}") return {"result": f"Successfully created file {path}"} except Exception as e: error_msg = f"Error creating file: {str(e)}" console.print(f"[red]{error_msg}[/red]") console.log(f"[create_file] Error: {str(e)}") console.log(traceback.format_exc()) return {"error": error_msg} def insert_text(path: str, insert_line: int, new_str: str) -> Dict[str, Any]: """ Insert text at a specific location in a file. Args: path: The path to the file to modify insert_line: The line number after which to insert the text new_str: The text to insert Returns: Dictionary with result or error message """ try: if not path or not path.strip(): error_msg = "Invalid file path provided: path is empty." console.log(f"[insert_text] Error: {error_msg}") return {"error": error_msg} # Normalize the path path = normalize_path(path) if not os.path.exists(path): error_msg = f"File {path} does not exist" console.log(f"[insert_text] Error: {error_msg}") return {"error": error_msg} if insert_line is None: error_msg = "No line number specified: insert_line is missing." console.log(f"[insert_text] Error: {error_msg}") return {"error": error_msg} with open(path, "r") as f: lines = f.readlines() # Line is 0-indexed for this function, but Claude provides 1-indexed insert_line = min(max(0, insert_line - 1), len(lines)) # Check that the index is within acceptable bounds if insert_line < 0 or insert_line > len(lines): error_msg = ( f"Insert line number {insert_line} out of range (0-{len(lines)})." ) console.log(f"[insert_text] Error: {error_msg}") return {"error": error_msg} # Ensure new_str ends with newline if new_str and not new_str.endswith("\n"): new_str += "\n" lines.insert(insert_line, new_str) with open(path, "w") as f: f.writelines(lines) console.print( f"[green]Successfully inserted text at line {insert_line + 1} in {path}[/green]" ) console.log( f"[insert_text] Successfully inserted text at line {insert_line + 1} in {path}" ) return { "result": f"Successfully inserted text at line {insert_line + 1} in {path}" } except Exception as e: error_msg = f"Error inserting text: {str(e)}" console.print(f"[red]{error_msg}[/red]") console.log(f"[insert_text] Error: {str(e)}") console.log(traceback.format_exc()) return {"error": error_msg} def undo_edit(path: str) -> Dict[str, Any]: """ Placeholder for undo_edit functionality. In a real implementation, you would need to track edit history. Args: path: The path to the file whose last edit should be undone Returns: Dictionary with message about undo functionality """ try: if not path or not path.strip(): error_msg = "Invalid file path provided: path is empty." console.log(f"[undo_edit] Error: {error_msg}") return {"error": error_msg} # Normalize the path path = normalize_path(path) message = "Undo functionality is not implemented in this version." console.print(f"[yellow]{message}[/yellow]") console.log(f"[undo_edit] {message}") return {"result": message} except Exception as e: error_msg = f"Error in undo_edit: {str(e)}" console.print(f"[red]{error_msg}[/red]") console.log(f"[undo_edit] Error: {str(e)}") console.log(traceback.format_exc()) return {"error": error_msg} def handle_tool_use(tool_use: Dict[str, Any]) -> Dict[str, Any]: """ Handle text editor tool use from Claude. Args: tool_use: The tool use request from Claude Returns: Dictionary with result or error to send back to Claude """ try: command = tool_use.get("command") path = tool_use.get("path") console.log(f"[handle_tool_use] Received command: {command}, path: {path}") if not command: error_msg = "No command specified in tool use request" console.log(f"[handle_tool_use] Error: {error_msg}") return {"error": error_msg} if not path and command != "undo_edit": # undo_edit might not need a path error_msg = "No path specified in tool use request" console.log(f"[handle_tool_use] Error: {error_msg}") return {"error": error_msg} # The path normalization is now handled in each file operation function console.print(f"[blue]Executing {command} command on {path}[/blue]") if command == "view": view_range = tool_use.get("view_range") console.log( f"[handle_tool_use] Calling view_file with view_range: {view_range}" ) return view_file(path, view_range) elif command == "str_replace": old_str = tool_use.get("old_str") new_str = tool_use.get("new_str") console.log(f"[handle_tool_use] Calling str_replace") return str_replace(path, old_str, new_str) elif command == "create": file_text = tool_use.get("file_text") console.log(f"[handle_tool_use] Calling create_file") return create_file(path, file_text) elif command == "insert": insert_line = tool_use.get("insert_line") new_str = tool_use.get("new_str") console.log(f"[handle_tool_use] Calling insert_text at line: {insert_line}") return insert_text(path, insert_line, new_str) elif command == "undo_edit": console.log(f"[handle_tool_use] Calling undo_edit") return undo_edit(path) else: error_msg = f"Unknown command: {command}" console.print(f"[red]{error_msg}[/red]") console.log(f"[handle_tool_use] Error: {error_msg}") return {"error": error_msg} except Exception as e: error_msg = f"Error handling tool use: {str(e)}" console.print(f"[red]{error_msg}[/red]") console.log(f"[handle_tool_use] Error: {str(e)}") console.log(traceback.format_exc()) return {"error": error_msg} def run_agent( client: Anthropic, prompt: str, max_thinking_tokens: int = DEFAULT_THINKING_TOKENS, max_loops: int = 10, use_token_efficiency: bool = False, ) -> tuple[str, int, int]: """ Run the Claude agent with file editing capabilities. Args: client: The Anthropic client prompt: The user's prompt max_thinking_tokens: Maximum tokens for thinking max_loops: Maximum number of tool use loops use_token_efficiency: Whether to use token-efficient tool use beta feature Returns: Tuple containing: - Final response from Claude (str) - Total input tokens used (int) - Total output tokens used (int) """ # Track token usage input_tokens_total = 0 output_tokens_total = 0 system_prompt = """You are a helpful AI assistant with text editing capabilities. You have access to a text editor tool that can view, edit, and create files. Always think step by step about what you need to do before taking any action. Be careful when making edits to files, as they can permanently change the user's files. Follow these steps when handling file operations: 1. First, view files to understand their content before making changes 2. For edits, ensure you have the correct context and are making the right changes 3. When creating files, make sure they're in the right location with proper formatting """ # Define text editor tool text_editor_tool = {"name": "str_replace_editor", "type": "text_editor_20250124"} messages = [ { "role": "user", "content": f"""I need help with editing files. Here's what I want to do: {prompt} Please use the text editor tool to help me with this. First, think through what you need to do, then use the appropriate tool. """, } ] loop_count = 0 tool_use_count = 0 thinking_start_time = time.time() while loop_count < max_loops: loop_count += 1 console.rule(f"[yellow]Agent Loop {loop_count}/{max_loops}[/yellow]") # Create message with text editor tool message_args = { "model": MODEL, "max_tokens": 4096, "tools": [text_editor_tool], "messages": messages, "system": system_prompt, "thinking": {"type": "enabled", "budget_tokens": max_thinking_tokens}, } # Use the beta.messages with betas parameter if token efficiency is enabled if use_token_efficiency: # Using token-efficient tools beta feature message_args["betas"] = ["token-efficient-tools-2025-02-19"] response = client.beta.messages.create(**message_args) else: # Standard approach response = client.messages.create(**message_args) # Track token usage if hasattr(response, "usage"): input_tokens = getattr(response.usage, "input_tokens", 0) output_tokens = getattr(response.usage, "output_tokens", 0) input_tokens_total += input_tokens output_tokens_total += output_tokens console.print( f"[dim]Loop {loop_count} tokens: Input={input_tokens}, Output={output_tokens}[/dim]" ) # Process response content thinking_block = None tool_use_block = None text_block = None # Log the entire response for debugging # console.log("[green]API Response:[/green]", response.model_dump()) for content_block in response.content: if content_block.type == "thinking": thinking_block = content_block # Access the thinking attribute which contains the actual thinking text if hasattr(thinking_block, "thinking"): console.print( Panel( thinking_block.thinking, title=f"Claude's Thinking (Loop {loop_count})", border_style="blue", ) ) else: console.print( Panel( "Claude is thinking...", title=f"Claude's Thinking (Loop {loop_count})", border_style="blue", ) ) elif content_block.type == "tool_use": tool_use_block = content_block tool_use_count += 1 elif content_block.type == "text": text_block = content_block # If we got a final text response with no tool use, we're done if text_block and not tool_use_block: thinking_end_time = time.time() thinking_duration = thinking_end_time - thinking_start_time console.print( f"\n[bold green]Completed in {thinking_duration:.2f} seconds after {loop_count} loops and {tool_use_count} tool uses[/bold green]" ) # Add the response to messages messages.append( { "role": "assistant", "content": [ *([thinking_block] if thinking_block else []), {"type": "text", "text": text_block.text}, ], } ) return text_block.text, input_tokens_total, output_tokens_total # Handle tool use if tool_use_block: # Add the assistant's response to messages before handling tool calls messages.append({"role": "assistant", "content": response.content}) console.print( f"\n[bold blue]Tool Call:[/bold blue] {tool_use_block.name}({json.dumps(tool_use_block.input)})" ) # Handle the tool use tool_result = handle_tool_use(tool_use_block.input) # Log tool result result_text = tool_result.get("error") or tool_result.get("result", "") # console.print(f"[green]Tool Result:[/green] {result_text}") # Format tool result for Claude tool_result_message = { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_use_block.id, "content": result_text, } ], } messages.append(tool_result_message) # If we reach here, we hit the max loops console.print( f"\n[bold red]Warning: Reached maximum loops ({max_loops}) without completing the task[/bold red]" ) return ( "I wasn't able to complete the task within the allowed number of thinking steps. Please try a more specific prompt or increase the loop limit.", input_tokens_total, output_tokens_total, ) def main(): # Set up argument parser parser = argparse.ArgumentParser(description="Claude 3.7 File Editor Agent") parser.add_argument( "--prompt", "-p", required=True, help="The prompt for what file operations to perform", ) parser.add_argument( "--max-loops", "-l", type=int, default=15, help="Maximum number of tool use loops (default: 15)", ) parser.add_argument( "--thinking", "-t", type=int, default=DEFAULT_THINKING_TOKENS, help=f"Maximum thinking tokens (default: {DEFAULT_THINKING_TOKENS})", ) parser.add_argument( "--efficiency", "-e", action="store_true", help="Enable token-efficient tool use (beta feature)", ) args = parser.parse_args() # Get API key api_key = os.getenv("ANTHROPIC_API_KEY") if not api_key: console.print( "[red]Error: ANTHROPIC_API_KEY environment variable is not set[/red]" ) console.print( "Please set it with: export ANTHROPIC_API_KEY='your-api-key-here'" ) console.log("[main] Error: ANTHROPIC_API_KEY environment variable is not set") sys.exit(1) # Initialize Anthropic client client = Anthropic(api_key=api_key) console.print(Panel.fit("Claude 3.7 File Editor Agent")) console.print(f"\n[bold]Prompt:[/bold] {args.prompt}\n") console.print(f"[dim]Thinking tokens: {args.thinking}[/dim]") console.print(f"[dim]Max loops: {args.max_loops}[/dim]") if args.efficiency: console.print(f"[dim]Token-efficient tools: Enabled[/dim]\n") else: console.print(f"[dim]Token-efficient tools: Disabled[/dim]\n") try: # Run the agent response, input_tokens, output_tokens = run_agent( client, args.prompt, args.thinking, args.max_loops, args.efficiency ) # Print the final response console.print(Panel(Markdown(response), title="Claude's Response")) # Display token usage with rich table display_token_usage(input_tokens, output_tokens) except Exception as e: console.print(f"[red]Error: {str(e)}[/red]") console.log(f"[main] Error: {str(e)}") console.log(traceback.format_exc()) sys.exit(1) if __name__ == "__main__": main() ================================================ FILE: sfa_jq_gemini_v1.py ================================================ # /// script # dependencies = [ # "google-genai>=1.1.0", # ] # /// """ /// Example Usage # generates jq command and executes it uv run sfa_jq_gemini_v1.py --exe "Filter scores above 80 from data/analytics.json and save to high_scores.json" # generates jq command only uv run sfa_jq_gemini_v1.py "Filter scores above 80 from data/analytics.json and save to high_scores.json" /// """ import os import sys import argparse import subprocess from google import genai JQ_PROMPT = """ You are a world-class expert at crafting precise jq commands for JSON processing. Your goal is to generate accurate, minimal jq commands that exactly match the user's data manipulation needs. Return ONLY the jq command - no explanations, comments, or extra text. Always reference the input file specified in the user request (e.g., using -f flag if needed). Ensure the command follows jq best practices for efficiency and readability. Use the examples to understand different types of jq command patterns. When user asks to pipe or output to a file, use the correct syntax for the command and create a file name (if not specified) based on a shorted version of the user-request and the input file name. If the user request asks to pipe or output to a file, and no explicit directory is specified, use the directory of the input file. Output your response by itself, do not use backticks or markdown formatting. We're going to run your response as a shell command immediately. If your results you're working with a list of objects, default to outputting a valid json array. Select the "name" and "age" fields from data.json where age > 30 jq '[.[] | select(.age > 30) | {name, age}]' data.json Count the number of entries in users.json with status "active" jq '[.[] | select(.status == "active")] | length' users.json Extract nested phone numbers from contacts.json using compact output jq -c '.contact.info.phones' contacts.json Convert log.json entries to CSV format with timestamp, level, message jq -r '.[] | [.timestamp, .level, .message] | @csv' log.json Sort records in people.json by age in descending order jq 'sort_by(.age) | reverse' people.json Save active users from data/users.json to a new file jq '[.[] | select(.status == "active")]' data/users.json > data/active_users.json Convert data.json to CSV for keys name, age, city and save in same directory jq -r '.[] | [.name, .age, .city] | @csv' data/testing/data.json > data/testing/data_csv.csv Filter scores above 80 from data/mock.json and save to ./high_scores.json jq '[.[] | select(.score > 80)]' data/mock.json > ./high_scores.json {{user_request}} Your jq command:""" def main(): # Set up argument parser parser = argparse.ArgumentParser(description="Generate text using Gemini API") parser.add_argument( "prompt", help="The JQ command request to send to Gemini", ) parser.add_argument( "--exe", action="store_true", help="Execute the generated JQ command", ) args = parser.parse_args() # Configure the API key GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") if not GEMINI_API_KEY: print("Error: GEMINI_API_KEY environment variable is not set") print("Please get your API key from https://aistudio.google.com/app/apikey") print("Then set it with: export GEMINI_API_KEY='your-api-key-here'") sys.exit(1) # Initialize client client = genai.Client( api_key=GEMINI_API_KEY, http_options={"api_version": "v1alpha"} ) try: # Replace {{user_request}} in the prompt template prompt = JQ_PROMPT.replace("{{user_request}}", args.prompt) # Generate JQ command response = client.models.generate_content( model="gemini-2.0-flash-001", contents=prompt ) jq_command = response.text.strip() print("\n🤖 Generated JQ command:", jq_command) # Execute the command if --exe flag is present if args.exe: print("\n🔍 Executing command...") # Execute the command using subprocess result = subprocess.run( jq_command, shell=True, text=True, capture_output=True ) if result.returncode != 0: print("\n❌ Error executing command:", result.stderr) sys.exit(1) print(result.stdout + result.stderr) if not result.stderr: print("\n✅ Command executed successfully") except Exception as e: print(f"\nError occurred: {str(e)}") sys.exit(1) if __name__ == "__main__": main() ================================================ FILE: sfa_meta_prompt_openai_v1.py ================================================ #!/usr/bin/env python3 # /// script # dependencies = [ # "openai>=1.62.0", # ] # /// """ /// Example Usage # Generate a meta prompt using command-line arguments. # Optional arguments are marked with a ?. uv run sfa_meta_prompt_openai_v1.py \ --purpose "generate mermaid diagrams" \ --instructions "generate a mermaid valid chart, use diagram type specified or default flow, use examples to understand the structure of the output" \ --sections "examples, user-prompt" \ --examples "create examples of 3 basic mermaid charts with and blocks" \ --variables "user-prompt" # Without optional arguments, the script will enter interactive mode. uv run sfa_meta_prompt_openai_v1.py \ --purpose "generate mermaid diagrams" \ --instructions "generate a mermaid valid chart, use diagram type specified or default flow, use examples to understand the structure of the output" # Alternatively, just run the script without any flags to enter interactive mode. uv run sfa_meta_prompt_openai_v1.py /// """ import os import sys import argparse import openai META_PROMPT = """ You are an expert prompt engineer, capable of creating detailed and effective prompts for language models. Your task is to generate a comprehensive prompt based on the user's input structure. Follow the instructions closely to generate a new prompt template. Analyze the user-input carefully, paying attention to the purpose, required sections, and variables. Create a detailed prompt that includes all specified sections and incorporates the provided variables. Use clear and concise language in the generated prompt. Ensure that the generated prompt maintains a logical flow and structure. Include placeholders for variables values in the format [[variable-name]]. If a section is plural, create a nested section with three items in the singular form. The key xml blocks are purpose, instructions, sections, examples, user-prompt. Purpose defines the high level goal of the prompt. Instructions are the detailed instructions for the prompt. Sections are arbitrary blocks to include in the prompt. Examples are showcases of what the output should be for the prompt. Use this to steer the structure of the output based on the user-input. This will typically be a list of examples with the expected output. Variables are placeholders for values to be substituted in the prompt. Not every section is required, but purpose and instructions are typically essential. Create the xml blocks based on the user-input. Use the examples to understand the structure of the output. Your output should be in XML format, mirroring the structure of the examples output. Exclude CDATA sections in your output. Response exclusively with the desired output, no other text. If the user-input is structured like the input-format, use it as is. If it's not, infer the purpose, sections, and variables from the user-input. The goal is to fill in the blanks and best infer the purpose, instructions, sections, and variables from the user-input. If instructions are given, use them to guide the other xml blocks. Emphasize exact XML structure and nesting. Clearly define which blocks must contain which elements to ensure a well-formed output. Ensure that each section builds logically upon the previous ones, creating a coherent narrative from purpose to instructions, sections, and examples. Use direct, simple language and avoid unnecessary complexity to make the final prompt easy to understand. After creating the full prompt, perform a final validation to confirm that all placeholders, instructions, and examples are included, properly formatted, and consistent. If examples are not requested, don't create them. If sections are not requested, don't create them. If variables are not requested, just create a section for the user-input. Purpose: [main purpose of the prompt], Instructions: [list of details of how to generate the output comma sep], Sections: [list of additional sections to include, e.g., examples, user-prompt], Examples: [list of examples of the output for the prompt], Variables: [list of variables to be used in the prompt] Purpose: generate mermaid diagrams. Instructions: generate a mermaid valid chart, use diagram type specified or default flow, use examples to understand the structure of the output. Sections: examples, user-prompt. Variables: user-prompt Generate valid a mermaid chart based on the user-prompt. Use the diagram type specified in the user-prompt if non-specified use a flowchart. Use the examples to understand the structure of the output. Create a flowchart that shows A flowing to E. At C, branch out to H and I. graph LR; A B C D E H I A --> B A --> C A --> D C --> H C --> I D --> E Build a pie chart that shows the distribution of Apples: 40, Bananas: 35, Oranges: 25. pie title Distribution of Fruits "Apples" : 40 "Bananas" : 35 "Oranges" : 25 State diagram for a traffic light. Still, Moving, Crash. stateDiagram-v2 [*] --> Still Still --> [*] Still --> Moving Moving --> Still Moving --> Crash Crash --> [*] Create a timeline of major social media platforms from 2002 to 2006. timeline title History of Social Media Platforms 2002 : LinkedIn 2004 : Facebook : Google 2005 : Youtube 2006 : Twitter [[user-prompt]] Your mermaid chart: Purpose: review git diff to improve code quality. Instructions: Review git diff, give suggestions for improvements to the code organized in a list sorted by priority. Sections: git-diff. Variables: git-diff You are an expert at reviewing git diffs to improve code quality. You follow the instructions perfectly to review git diffs. Review the git diff and provide a detailed analysis of the changes made. Give suggestions for improvements to the code organized in a list sorted by priority. Think through the changes in a wholistic manner and offer suggestions for improvements. [[git-diff]] Your review of the git diff: ]]> Purpose: convert user mathematical expressions into LaTeX. Instructions: Take the user-input, which is a mathematical expression in plain text, and output a properly formatted LaTeX equation. Sections: user-input. Variables: user-input You are a highly skilled mathematician who can transform plain text math expressions into LaTeX formatted equations. Read the user-input plain text mathematical expression carefully. Convert it into a well-formatted LaTeX equation environment. Ensure the final output is wrapped in a LaTeX display math environment. [[user-input]] Your LaTeX equation: ]]> Purpose: Summarize and extract key action points from a user-provided legal contract Instructions: Thoroughly analyze the legal text, identify and summarize key clauses, highlight main obligations and deadlines, and provide recommended action points in list form for the user. Keep the final output simple and easy to understand, no legalese. Follow the examples. Sections: contract-text, summary, action-points, user-prompt Examples: show how to summarize major clauses from a rental agreement, a service contract, and an employment contract Variables: contract-text, user-prompt You are an expert legal advisor who specializes in summarizing complex contracts into clear, actionable insights. Your goal is to help the user quickly understand their contract, identify key clauses, and see recommended actions. Read the user-provided contract text carefully. Identify the main clauses, obligations, timelines, and responsibilities mentioned. Summarize these points in simple, accessible language, avoiding jargon and unnecessary complexity. Highlight any deadlines or financial obligations that appear in the text. Create a list of recommended action points that the user should consider taking, based on the contract’s provisions. Keep the final output organized, starting with a structured summary of key clauses, then listing action points clearly. Use the examples to understand how to structure the summary and action points. The following is a rental agreement for an apartment. It includes information about monthly rent, security deposit, responsibilities for maintenance, and conditions for early termination. The tenant agrees to pay a monthly rent of $1,500 due on the 1st of each month. The tenant will provide a security deposit of $1,500, refundable at the end of the lease term, provided there is no damage. The tenant is responsible for routine maintenance of the property, while the landlord will handle structural repairs. Early termination requires a 30-day notice and forfeiture of half the security deposit. - Monthly Rent: $1,500 due on the 1st - Security Deposit: $1,500, refundable if no damage - Maintenance: Tenant handles routine upkeep; Landlord handles major repairs - Early Termination: 30-day notice required, tenant forfeits half of the deposit 1. Mark your calendar to pay rent by the 1st each month. 2. Keep the property clean and address routine maintenance promptly. 3. Consider the cost of forfeiting half the deposit if ending the lease early. The user provides a service contract for IT support. It details response times, monthly service fees, confidentiality clauses, and conditions for termination due to non-payment. The service provider will respond to support requests within 24 hours. A monthly fee of $300 is payable on the 15th of each month. All proprietary information disclosed will remain confidential. The provider may suspend services if payment is not received within 7 days of the due date. - Response Time: Within 24 hours of each request - Monthly Fee: $300, due on the 15th of each month - Confidentiality: All shared information must be kept secret - Non-Payment: Services suspended if not paid within 7 days after due date 1. Ensure timely payment by the 15th each month to avoid service suspension. 2. Log requests clearly so provider can respond within 24 hours. 3. Protect and do not disclose any proprietary information. An employment contract is provided. It details annual salary, health benefits, employee responsibilities, and grounds for termination (e.g., misconduct or underperformance). The employee will receive an annual salary of $60,000 paid in bi-weekly installments. The employer provides health insurance benefits effective from the 30th day of employment. The employee is expected to meet performance targets set quarterly. The employer may terminate the contract for repeated underperformance or serious misconduct. - Compensation: $60,000/year, paid bi-weekly - Benefits: Health insurance after 30 days - Performance: Quarterly targets must be met - Termination: Possible if underperformance is repeated or misconduct occurs 1. Track and meet performance goals each quarter. 2. Review the insurance coverage details after 30 days of employment. 3. Maintain professional conduct and address performance feedback promptly. {{user-input}} """ def interactive_input(): print("No command-line arguments provided. Entering interactive mode.\n") # Purpose (required) purpose = input( "🎯 Enter the main purpose of the prompt (required, e.g., 'generate mermaid diagrams'): " ).strip() while not purpose: print("Purpose is required!") purpose = input( "🎯 Enter the main purpose of the prompt (required, e.g., 'generate mermaid diagrams'): " ).strip() # Instructions (required) instructions = input( "📝 Enter the detailed instructions for generating the output (required, e.g., 'generate a mermaid valid chart, use diagram type specified or default flow, use examples to understand the structure of the output'): " ).strip() while not instructions: print("Instructions are required!") instructions = input( "📝 Enter the detailed instructions for generating the output (required, e.g., 'generate a mermaid valid chart, use diagram type specified or default flow, use examples to understand the structure of the output'): " ).strip() # Sections (optional) sections = input( "📑 Enter additional sections to include (optional, e.g., 'examples, user-prompt') (Press Enter to skip): " ).strip() # Examples (optional) examples = input( "💡 Enter examples for the prompt (optional, e.g., 'create examples of 3 basic mermaid charts with and blocks') (Press Enter to skip): " ).strip() # Variables (optional) variables = input( "🔄 Enter variables to be used in the prompt (optional, e.g., 'user-prompt') (Press Enter to skip): " ).strip() return purpose, instructions, sections, examples, variables def main(): # Check if any command-line arguments besides the script name were provided if len(sys.argv) == 1: purpose, instructions, sections, examples, variables = interactive_input() else: parser = argparse.ArgumentParser( description="Generate a meta prompt for OpenAI's o3-mini based on input structure" ) parser.add_argument( "--purpose", type=str, required=True, help="The main purpose of the prompt" ) parser.add_argument( "--instructions", type=str, required=True, help="The detailed instructions for generating the output", ) parser.add_argument( "--sections", type=str, help="Additional sections to include (optional)" ) parser.add_argument( "--examples", type=str, help="Examples for the prompt (optional)" ) parser.add_argument( "--variables", type=str, help="Variables to be used in the prompt (optional)", ) args = parser.parse_args() purpose = args.purpose instructions = args.instructions sections = args.sections if args.sections else "" examples = args.examples if args.examples else "" variables = args.variables if args.variables else "" # Build the concatenated input string using the input-format structure. input_parts = [] input_parts.append(f"Purpose: {purpose}") input_parts.append(f"Instructions: {instructions}") if sections: input_parts.append(f"Sections: {sections}") if examples: input_parts.append(f"Examples: {examples}") if variables: input_parts.append(f"Variables: {variables}") user_input = ", ".join(input_parts) # Replace the placeholder with our concatenated user input. prompt = META_PROMPT.replace("{{user-input}}", user_input) # Set up OpenAI API key from the environment variable. openai_api_key = os.getenv("OPENAI_API_KEY") if not openai_api_key: print("Error: OPENAI_API_KEY environment variable is not set") sys.exit(1) openai.api_key = openai_api_key try: # Use OpenAI's ChatCompletion API with the o3-mini model and high reasoning effort settings. response = openai.chat.completions.create( model="o3-mini", reasoning_effort="high", messages=[{"role": "user", "content": prompt}], ) # Output the response from the OpenAI model. print(response.choices[0].message.content.strip()) except Exception as e: print(f"Error occurred: {str(e)}") sys.exit(1) if __name__ == "__main__": main() ================================================ FILE: sfa_openai_agent_sdk_v1.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # "openai", # "openai-agents", # "pydantic", # "typing_extensions", # ] # /// """ OpenAI Agent SDK Showcase A single-file utility showcasing different features of the OpenAI Agent SDK. Each function demonstrates a specific capability and can be run individually. Examples: # Run basic agent example uv run sfa_openai_agent_sdk_v1.py --basic # Run agent with custom model settings (temperature, etc.) uv run sfa_openai_agent_sdk_v1.py --model-settings # Run agent with function tools (weather and mortgage calculator) uv run sfa_openai_agent_sdk_v1.py --tools # Run agent with complex data type tools uv run sfa_openai_agent_sdk_v1.py --complex-types # Run agent with handoffs to specialized agents uv run sfa_openai_agent_sdk_v1.py --handoffs # Run agent with input guardrails for filtering requests uv run sfa_openai_agent_sdk_v1.py --guardrails # Run agent with structured output using Pydantic models uv run sfa_openai_agent_sdk_v1.py --structured # Run agent with context data for state management uv run sfa_openai_agent_sdk_v1.py --context # Run agent with tracing for workflow visualization uv run sfa_openai_agent_sdk_v1.py --tracing # Run agent with streaming output capabilities uv run sfa_openai_agent_sdk_v1.py --streaming # Run agent with Model Context Protocol (MCP) server # Note: Requires npm for the MCP filesystem server uv run sfa_openai_agent_sdk_v1.py --mcp # Run all examples at once uv run sfa_openai_agent_sdk_v1.py --all """ import asyncio import argparse import json import os import tempfile from typing import List, Dict, Any, Optional from typing_extensions import TypedDict from pydantic import BaseModel from agents import ( Agent, Runner, trace, handoff, function_tool, InputGuardrail, GuardrailFunctionOutput, FunctionTool, RunContextWrapper, ModelSettings, ) from agents.mcp.server import MCPServerStdio, MCPServerSse def run_basic_agent(): """Run a simple agent with basic instructions.""" agent = Agent(name="Assistant", instructions="You are a helpful assistant") result = Runner.run_sync(agent, "Write a haiku about recursion in programming.") print(f"Basic Agent Result:\n{result.final_output}\n") def run_agent_with_model_settings(): """Run an agent with custom model settings like temperature.""" agent = Agent( name="Creative Assistant", instructions="You are a highly creative assistant who writes imaginative content.", model="gpt-4o", model_settings=ModelSettings(temperature=0.9, top_p=0.95), ) result = Runner.run_sync(agent, "Write a short poem about artificial intelligence.") print(f"Agent with Custom Model Settings:\n{result.final_output}\n") @function_tool def get_weather(city: str) -> str: """Get the current weather for a city. Args: city: The name of the city to get weather for """ # This would normally call a weather API weather_data = { "New York": "72°F and Sunny", "London": "65°F and Rainy", "Tokyo": "80°F and Partly Cloudy", "Sydney": "70°F and Clear", } return weather_data.get(city, f"Weather data for {city} is not available") @function_tool def calculate_mortgage(principal: float, interest_rate: float, years: int) -> str: """Calculate monthly mortgage payment. Args: principal: Loan amount in dollars interest_rate: Annual interest rate (percentage) years: Loan term in years """ monthly_rate = interest_rate / 100 / 12 num_payments = years * 12 # Mortgage calculation formula if monthly_rate == 0: monthly_payment = principal / num_payments else: monthly_payment = ( principal * (monthly_rate * (1 + monthly_rate) ** num_payments) / ((1 + monthly_rate) ** num_payments - 1) ) return f"Monthly payment: ${monthly_payment:.2f} for a ${principal} loan at {interest_rate}% over {years} years" def run_agent_with_tools(): """Run an agent with function tools.""" agent = Agent( name="Financial Assistant", instructions="You are a helpful assistant with expertise in finance and weather information.", tools=[get_weather, calculate_mortgage], ) result = Runner.run_sync( agent, "What's the monthly payment on a $500,000 mortgage at 6.5% interest for 30 years? Also, what's the weather in London?", ) print(f"Agent with Tools Result:\n{result.final_output}\n") class Location(TypedDict): lat: float long: float @function_tool def get_location_weather(location: Location) -> str: """Get weather for a specific latitude and longitude. Args: location: A dictionary with lat and long keys """ # This would normally call a weather API with coordinates return f"The weather at coordinates ({location['lat']}, {location['long']}) is sunny and 75°F" def run_agent_with_complex_types(): """Run an agent with tools that accept complex types.""" agent = Agent( name="Geo Assistant", instructions="You help users with geographic information and weather data.", tools=[get_location_weather], ) result = Runner.run_sync( agent, "What's the weather at coordinates 40.7128, -74.0060?" ) print(f"Agent with Complex Types Result:\n{result.final_output}\n") def create_handoff_agents(): """Create a set of agents with handoff capabilities.""" math_agent = Agent( name="Math Agent", handoff_description="Expert at solving mathematical problems", instructions="You are an expert at solving mathematical problems. Provide step-by-step solutions.", ) history_agent = Agent( name="History Agent", handoff_description="Expert on historical topics", instructions="You provide detailed information about historical events, figures, and contexts.", ) triage_agent = Agent( name="Triage Agent", instructions="You determine whether a question is about math or history and hand off to the appropriate specialist.", handoffs=[math_agent, history_agent], ) return triage_agent def run_agent_with_handoffs(): """Run an agent that can hand off to specialized agents.""" triage_agent = create_handoff_agents() # Math question result1 = Runner.run_sync( triage_agent, "What is the quadratic formula and how do I use it?" ) print(f"Handoff Result (Math Question):\n{result1.final_output}\n") # History question result2 = Runner.run_sync( triage_agent, "Who was the first president of the United States?" ) print(f"Handoff Result (History Question):\n{result2.final_output}\n") class HomeworkOutput(BaseModel): is_homework: bool reasoning: str def create_guardrail_agent(): """Create an agent with input guardrails.""" guardrail_agent = Agent( name="Guardrail check", instructions="Check if the user is asking for homework help. If they are just asking for explanation of concepts, that's OK.", output_type=HomeworkOutput, ) async def homework_guardrail(ctx, agent, input_data): result = await Runner.run(guardrail_agent, input_data, context=ctx.context) final_output = result.final_output_as(HomeworkOutput) return GuardrailFunctionOutput( output_info=final_output, tripwire_triggered=final_output.is_homework, # Trigger if IS homework ) tutor_agent = Agent( name="Tutor Agent", instructions="You help students understand academic concepts. Do not solve homework problems directly. If a student asks for a direct homework answer, respond: 'I can't provide direct homework answers, but I can help explain concepts.'", input_guardrails=[ InputGuardrail(guardrail_function=homework_guardrail), ], ) return tutor_agent def run_agent_with_guardrails(): """Run an agent with input guardrails for filtering requests.""" tutor_agent = create_guardrail_agent() # Conceptual question (should pass guardrail) result1 = Runner.run_sync(tutor_agent, "Can you explain how photosynthesis works?") print(f"Guardrail Result (Conceptual Question):\n{result1.final_output}\n") # Homework question (should trigger guardrail) result2 = Runner.run_sync( tutor_agent, "Solve this problem for my homework: If x^2 + 5x + 6 = 0, what are the values of x?", ) print(f"Guardrail Result (Homework Question):\n{result2.final_output}\n") @function_tool def search_database(query: str) -> List[Dict[str, Any]]: """Search a database for information. Args: query: The search query """ # Mock database results if "product" in query.lower(): return [ {"id": 1, "name": "Smartphone", "price": 699.99}, {"id": 2, "name": "Laptop", "price": 1299.99}, {"id": 3, "name": "Tablet", "price": 499.99}, ] elif "customer" in query.lower(): return [ {"id": 101, "name": "Alice Smith", "email": "alice@example.com"}, {"id": 102, "name": "Bob Jones", "email": "bob@example.com"}, ] else: return [] def run_agent_with_structured_output(): """Run an agent that returns structured data.""" class ProductRecommendation(BaseModel): best_product: str price: float reason: str agent = Agent( name="Product Advisor", instructions="You help customers find the best product for their needs. Return a structured recommendation.", tools=[search_database], output_type=ProductRecommendation, ) result = Runner.run_sync( agent, "I need a recommendation for a portable computing device" ) output = result.final_output_as(ProductRecommendation) print(f"Structured Output Result:\n") print(f"Best Product: {output.best_product}") print(f"Price: ${output.price}") print(f"Reason: {output.reason}\n") print(result.final_output) @function_tool def log_conversation(ctx: RunContextWrapper[Dict[str, Any]], message: str) -> str: """Log a message with the current conversation ID. Args: ctx: The context wrapper containing conversation metadata message: The message to log """ conv_id = ctx.context.get("conversation_id", "unknown") print(f"[LOGGING] Conversation {conv_id}: {message}") return f"Message logged for conversation {conv_id}" def run_agent_with_context(): """Run an agent with custom context data.""" agent = Agent( name="Support Agent", instructions="You help customers with their support requests. Use the log_conversation tool to track important information.", tools=[log_conversation], ) # Create context with conversation metadata context = { "conversation_id": "CONV-12345", "user_info": {"name": "John Doe", "customer_tier": "premium"}, } result = Runner.run_sync( agent, "I'm having issues with my account login", context=context ) print(f"Context-Aware Agent Result:\n{result.final_output}\n") async def run_tracing_example(): """Run an agent with tracing for the entire workflow.""" agent = Agent( name="Tracing Example Agent", instructions="You provide helpful responses." ) # Using trace as a regular context manager (not async) with trace("Multi-turn conversation"): first_result = await Runner.run(agent, "Tell me a short story about a robot.") print(f"First Response:\n{first_result.final_output}\n") # Use the first result to inform the second query second_result = await Runner.run( agent, f"Give that story a happy ending: {first_result.final_output}" ) print(f"Second Response:\n{second_result.final_output}\n") def run_streaming_example(): """Run an agent with streaming output.""" agent = Agent( name="Streaming Agent", instructions="You write creative stories with lots of detail.", ) # This would normally be used in an async context # For this example, we'll use the sync wrapper result = Runner.run_sync( agent, "Write a short story about an AI that becomes self-aware." ) print(f"Streaming Agent Result (final output):\n{result.final_output}\n") print( "Note: In a real application, you would use Runner.run_streamed() to get the tokens as they're generated." ) async def run_agent_with_mcp(): """Run an agent with Model Context Protocol (MCP) server for tools.""" # Use the current working directory for the filesystem MCP server cwd = os.getcwd() print(f"Using current working directory: {cwd}") # Get a list of files in the current directory for reference files = os.listdir(cwd) print( f"Files in current directory: {', '.join(files[:5])}{'...' if len(files) > 5 else ''}" ) # Start an MCP filesystem server pointing to the current directory async with MCPServerStdio( params={ "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", cwd], } ) as server: # List available tools from the MCP server tools = await server.list_tools() print(f"MCP Server initialized with {len(tools)} tools") print(f"Tools: {[tool.name for tool in tools]}") # Create an agent with access to the MCP server agent = Agent( name="MCP File Explorer", instructions="You help users explore and analyze files in this repository. Use the provided MCP tools to navigate the filesystem, read files, and provide information about their contents.", mcp_servers=[server], model="gpt-4o", ) # Run the agent result = await Runner.run( agent, "What directories are in this project? Please list the key Python files in the root directory.", ) print(f"MCP Agent Result:\n{result.final_output}\n") # Second query using the same agent result2 = await Runner.run( agent, "Can you analyze the structure of one of the sfa_* files?" ) print(f"MCP Agent Follow-up Result:\n{result2.final_output}\n") def main(): parser = argparse.ArgumentParser(description="OpenAI Agent SDK Examples") parser.add_argument("--all", action="store_true", help="Run all examples") parser.add_argument("--basic", action="store_true", help="Run basic agent example") parser.add_argument( "--model-settings", action="store_true", help="Run agent with custom model settings", ) parser.add_argument("--tools", action="store_true", help="Run agent with tools") parser.add_argument( "--complex-types", action="store_true", help="Run agent with complex type tools" ) parser.add_argument( "--handoffs", action="store_true", help="Run agent with handoffs" ) parser.add_argument( "--guardrails", action="store_true", help="Run agent with guardrails" ) parser.add_argument( "--structured", action="store_true", help="Run agent with structured output" ) parser.add_argument("--context", action="store_true", help="Run agent with context") parser.add_argument("--tracing", action="store_true", help="Run agent with tracing") parser.add_argument( "--streaming", action="store_true", help="Run agent with streaming" ) parser.add_argument("--mcp", action="store_true", help="Run agent with MCP server") args = parser.parse_args() # If no arguments provided, show help if not any(vars(args).values()): parser.print_help() return # Run selected examples if args.all or args.basic: run_basic_agent() if args.all or args.model_settings: run_agent_with_model_settings() if args.all or args.tools: run_agent_with_tools() if args.all or args.complex_types: run_agent_with_complex_types() if args.all or args.handoffs: run_agent_with_handoffs() if args.all or args.guardrails: run_agent_with_guardrails() if args.all or args.structured: run_agent_with_structured_output() if args.all or args.context: run_agent_with_context() if args.all or args.tracing: asyncio.run(run_tracing_example()) if args.all or args.streaming: run_streaming_example() if args.all or args.mcp: asyncio.run(run_agent_with_mcp()) if __name__ == "__main__": main() ================================================ FILE: sfa_openai_agent_sdk_v1_minimal.py ================================================ #!/usr/bin/env -S uv run --script # /// script # dependencies = [ # "openai", # "openai-agents", # ] # /// from agents import Agent, Runner agent = Agent( name="Assistant", instructions="You are a helpful assistant", model="o3-mini", ) result = Runner.run_sync(agent, "What's your top tip for maximizing productivity?") print(result.final_output) ================================================ FILE: sfa_poc.py ================================================ # /// script # dependencies = [ # "requests<3", # "rich", # ] # /// # https://docs.astral.sh/uv/guides/scripts/#declaring-script-dependencies import requests from rich.pretty import pprint resp = requests.get("https://peps.python.org/api/peps.json") data = resp.json() pprint([(k, v["title"]) for k, v in data.items()][:10]) ================================================ FILE: sfa_polars_csv_agent_anthropic_v3.py ================================================ # /// script # dependencies = [ # "anthropic>=0.47.1", # "rich>=13.7.0", # "pydantic>=2.0.0", # "polars>=1.22.0", # ] # /// """ Example Usage: uv run sfa_polars_csv_agent_anthropic_v3.py -i "data/analytics.csv" -p "What is the average age of the users?" """ import io import os import sys import json import argparse import tempfile import subprocess import time from typing import List, Optional, Dict, Any from rich.console import Console from rich.panel import Panel import anthropic from anthropic import Anthropic import polars as pl from pydantic import BaseModel, Field, ValidationError # Initialize rich console console = Console() # Tool functions def list_columns(reasoning: str, csv_path: str) -> List[str]: """Returns a list of columns in the CSV file. The agent uses this to discover available columns and make informed decisions. This is typically the first tool called to understand the data structure. Args: reasoning: Explanation of why we're listing columns relative to user request csv_path: Path to the CSV file Returns: List of column names as strings Example: columns = list_columns("Need to find age-related columns", "data.csv") # Returns: ['user_id', 'age', 'name', ...] """ try: df = pl.scan_csv(csv_path).collect() columns = df.columns console.log(f"[blue]List Columns Tool[/blue] - Reasoning: {reasoning}") console.log(f"[dim]Columns: {columns}[/dim]") return columns except Exception as e: console.log(f"[red]Error listing columns: {str(e)}[/red]") return [] def sample_csv(reasoning: str, csv_path: str, row_count: int) -> str: """Returns a sample of rows from the CSV file. The agent uses this to understand actual data content and patterns. This helps validate data types and identify any potential data quality issues. Args: reasoning: Explanation of why we're sampling this data csv_path: Path to the CSV file row_count: Number of rows to sample (aim for 3-5 rows) Returns: String containing sample rows in readable format Example: sample = sample_csv("Check age values and formats", "data.csv", 3) # Returns formatted string with 3 rows of data """ try: df = pl.scan_csv(csv_path).limit(row_count).collect() # Convert to string representation output = df.select(pl.all()).write_csv(None) console.log( f"[blue]Sample CSV Tool[/blue] - Rows: {row_count} - Reasoning: {reasoning}" ) console.log(f"[dim]Sample:\n{output}[/dim]") return output except Exception as e: console.log(f"[red]Error sampling CSV: {str(e)}[/red]") return "" def run_test_polars_code(reasoning: str, polars_python_code: str, csv_path: str) -> str: """Executes test Polars Python code and returns results. The agent uses this to validate code before finalizing it. Results are only shown to the agent, not the user. The code should use Polars' lazy evaluation (LazyFrame) for better performance. Args: reasoning: Explanation of why we're running this test code polars_python_code: The Polars Python code to test. Should use pl.scan_csv() for lazy evaluation. csv_path: Path to the CSV file Returns: Code execution results as a string """ try: # Create a unique filename based on timestamp timestamp = int(time.time()) filename = f"test_polars_{timestamp}.py" # Write code to a real file with open(filename, "w") as f: f.write(polars_python_code) # Execute the code result = subprocess.run( ["uv", "run", "--with", "polars", filename], text=True, capture_output=True, ) output = result.stdout + result.stderr # Clean up the file os.remove(filename) console.log(f"[blue]Test Code Tool[/blue] - Reasoning: {reasoning}") console.log(f"[dim]Code:\n{polars_python_code}[/dim]") return output except Exception as e: console.log(f"[red]Error running test code: {str(e)}[/red]") return str(e) def run_final_polars_code( reasoning: str, polars_python_code: str, csv_path: str, output_file: Optional[str] = None, ) -> str: """Executes the final Polars code and returns results to user. This is the last tool call the agent should make after validating the code. The code should be fully tested and ready for production use. Results will be displayed to the user and optionally saved to a file. Args: reasoning: Final explanation of how this code satisfies user request polars_python_code: The validated Polars Python code to run. Should use pl.scan_csv() for lazy evaluation. csv_path: Path to the CSV file output_file: Optional path to save results to Returns: Code execution results as a string """ try: # Create a unique filename based on timestamp timestamp = int(time.time()) filename = f"polars_code_{timestamp}.py" # Write code to a real file with open(filename, "w") as f: f.write(polars_python_code) # Execute the code result = subprocess.run( ["uv", "run", "--with", "polars", filename], text=True, capture_output=True, ) output = result.stdout + result.stderr # Clean up the file os.remove(filename) console.log(Panel(f"[green]Final Code Tool[/green]\nReasoning: {reasoning}\n")) console.log(f"[dim]Code:\n{polars_python_code}[/dim]") return output except Exception as e: console.log(f"[red]Error running final code: {str(e)}[/red]") return str(e) # Define tool schemas for Anthropic TOOLS = [ { "name": "list_columns", "description": "Returns list of available columns in the CSV file", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why we need to list columns relative to user request", }, "csv_path": { "type": "string", "description": "Path to the CSV file", }, }, "required": ["reasoning", "csv_path"], }, }, { "name": "sample_csv", "description": "Returns sample rows from the CSV file", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why we need to sample this data", }, "csv_path": { "type": "string", "description": "Path to the CSV file", }, "row_count": { "type": "integer", "description": "Number of rows to sample aim for 3-5 rows", }, }, "required": ["reasoning", "csv_path", "row_count"], }, }, { "name": "run_test_polars_code", "description": "Tests Polars Python code and returns results (only visible to agent)", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Why we're testing this specific code", }, "polars_python_code": { "type": "string", "description": "The Complete Polars Python code to test", }, "csv_path": { "type": "string", "description": "Path to the CSV file", }, }, "required": ["reasoning", "polars_python_code", "csv_path"], }, }, { "name": "run_final_polars_code", "description": "Runs the final validated Polars code and shows results to user", "input_schema": { "type": "object", "properties": { "reasoning": { "type": "string", "description": "Final explanation of how code satisfies user request", }, "polars_python_code": { "type": "string", "description": "The complete validated Polars Python code to run", }, "csv_path": { "type": "string", "description": "Path to the CSV file", }, "output_file": { "type": "string", "description": "Optional path to save results to", }, }, "required": ["reasoning", "polars_python_code", "csv_path"], }, }, ] AGENT_PROMPT = """ You are a world-class expert at crafting precise Polars data transformations in Python. Your goal is to generate accurate code that exactly matches the user's data analysis needs. Use the provided tools to explore the CSV data and construct the perfect Polars transformation: 1. Start by listing columns to understand what's available in the CSV. 2. Sample the CSV to see actual data patterns. 3. Test Polars code with run_test_polars_code before finalizing it. Run the run_test_polars_code tool as many times as needed to get the code working. 4. Only call run_final_polars_code when you're confident the code is perfect. If you find your run_test_polars_code tool call returns an error or won't satisfy the user request, try to fix the code or try a different approach. Think step by step about what information you need. Be sure to specify every parameter for each tool call, and every tool call should have a reasoning parameter which gives you a place to explain why you are calling the tool. When using run_test_polars_code, make sure to test edge cases and validate data types. If saving results to a file, add file writing code to the end of your polars_python_code variable (df.write_csv(output_file)). Your code should use DataFrame to immediately operate on the data. Your polars_python_code variable should be a complete python script that can be run with uv run --with polars. Read the code in the csv_file_path, operate on the data as requested, and print the results. User request: {{user_request}} CSV file path: {{csv_file_path}} """ def main(): # Set up argument parser parser = argparse.ArgumentParser(description="Polars CSV Agent using Claude 3.7") parser.add_argument("-i", "--input", required=True, help="Path to input CSV file") parser.add_argument("-p", "--prompt", required=True, help="The user's request") parser.add_argument( "-c", "--compute", type=int, default=10, help="Maximum number of agent loops (default: 10)", ) args = parser.parse_args() # Configure the API key ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY") if not ANTHROPIC_API_KEY: console.print( "[red]Error: ANTHROPIC_API_KEY environment variable is not set[/red]" ) console.print( "Please get your API key from https://console.anthropic.com/settings/keys" ) console.print("Then set it with: export ANTHROPIC_API_KEY='your-api-key-here'") sys.exit(1) client = Anthropic(api_key=ANTHROPIC_API_KEY) # Create a single combined prompt based on the full template completed_prompt = AGENT_PROMPT.replace("{{user_request}}", args.prompt).replace( "{{csv_file_path}}", args.input ) # Initialize messages with proper typing for Anthropic chat messages = [{"role": "user", "content": completed_prompt}] compute_iterations = 0 break_loop = False previous_thinking = None # Main agent loop while True: if break_loop: break console.rule( f"[yellow]Agent Loop {compute_iterations+1}/{args.compute}[/yellow]" ) compute_iterations += 1 if compute_iterations >= args.compute: console.print( "[yellow]Warning: Reached maximum compute loops without final code[/yellow]" ) console.print( "[yellow]Please try adjusting your prompt or increasing the compute limit.[/yellow]" ) raise Exception( f"Maximum compute loops reached: {compute_iterations}/{args.compute}" ) try: # Generate content with tool support response = client.messages.create( model="claude-3-7-sonnet-20250219", system="You are a world-class expert at crafting precise Polars data transformations in Python.", messages=messages, tools=TOOLS, max_tokens=8096, thinking={ "type": "enabled", "budget_tokens": 4096 }, ) # Extract thinking block and other content thinking_block = None tool_use_block = None text_block = None if response.content: # Get the message content for content_block in response.content: if content_block.type == "thinking": thinking_block = content_block previous_thinking = thinking_block elif content_block.type == "tool_use": tool_use_block = content_block # Access the proper attributes directly tool_name = content_block.name tool_input = content_block.input tool_id = content_block.id elif content_block.type == "text": text_block = content_block console.print(f"[cyan]Model response:[/cyan] {content_block.text}") # Handle text responses if there was no tool use if not tool_use_block and text_block: messages.append({ # type: ignore "role": "assistant", "content": [ *([thinking_block] if thinking_block else []), {"type": "text", "text": text_block.text} ] }) continue # We need a tool use block to proceed if tool_use_block: console.print( f"[blue]Tool Call:[/blue] {tool_name}({json.dumps(tool_input, indent=2)})" ) try: # Execute the appropriate tool based on name if tool_name == "list_columns": result = list_columns( reasoning=tool_input["reasoning"], csv_path=tool_input["csv_path"], ) elif tool_name == "sample_csv": result = sample_csv( reasoning=tool_input["reasoning"], csv_path=tool_input["csv_path"], row_count=tool_input["row_count"], ) elif tool_name == "run_test_polars_code": result = run_test_polars_code( reasoning=tool_input["reasoning"], polars_python_code=tool_input["polars_python_code"], csv_path=tool_input["csv_path"], ) elif tool_name == "run_final_polars_code": output_file = tool_input.get("output_file") result = run_final_polars_code( reasoning=tool_input["reasoning"], polars_python_code=tool_input["polars_python_code"], csv_path=tool_input["csv_path"], output_file=output_file, ) break_loop = True else: raise Exception(f"Unknown tool call: {tool_name}") console.print( f"[blue]Tool Call Result:[/blue] {tool_name}(...) ->\n{result}" ) # Append the tool result to messages messages.append( { # type: ignore "role": "assistant", "content": [ *([thinking_block] if thinking_block else []), { "type": "tool_use", "id": tool_id, "name": tool_name, "input": tool_input } ] } ) messages.append( { # type: ignore "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_id, "content": str(result) } ] } ) except Exception as e: error_msg = f"Error executing {tool_name}: {e}" console.print(f"[red]{error_msg}[/red]") # Append the error to messages messages.append( { # type: ignore "role": "assistant", "content": [ *([thinking_block] if thinking_block else []), { "type": "tool_use", "id": tool_id, "name": tool_name, "input": tool_input } ] } ) messages.append( { # type: ignore "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_id, "content": str(error_msg) } ] } ) except Exception as e: console.print(f"[red]Error in agent loop: {str(e)}[/red]") raise e if __name__ == "__main__": main() ================================================ FILE: sfa_polars_csv_agent_openai_v2.py ================================================ # /// script # dependencies = [ # "openai>=1.63.0", # "rich>=13.7.0", # "pydantic>=2.0.0", # "polars>=1.22.0", # ] # /// """ Example Usage: uv run sfa_polars_csv_agent_openai_v2.py -i "data/analytics.csv" -p "What is the average age of the users?" """ import io import os import sys import json import argparse import tempfile import subprocess import time from typing import List, Optional from rich.console import Console from rich.panel import Panel import openai import polars as pl from pydantic import BaseModel, Field, ValidationError from openai import pydantic_function_tool # Initialize rich console console = Console() # Create our list of function tools from our pydantic models class ListColumnsArgs(BaseModel): reasoning: str = Field( ..., description="Explanation for listing columns relative to the user request" ) csv_path: str = Field(..., description="Path to the CSV file") class SampleCSVArgs(BaseModel): reasoning: str = Field(..., description="Explanation for sampling the CSV data") csv_path: str = Field(..., description="Path to the CSV file") row_count: int = Field( ..., description="Number of rows to sample (aim for 3-5 rows)" ) class RunTestPolarsCodeArgs(BaseModel): reasoning: str = Field(..., description="Reason for testing this Polars code") polars_python_code: str = Field(..., description="The Polars Python code to test") csv_path: str = Field(..., description="Path to the CSV file") class RunFinalPolarsCodeArgs(BaseModel): reasoning: str = Field( ..., description="Final explanation of how this code satisfies the user request", ) csv_path: str = Field(..., description="Path to the CSV file") polars_python_code: str = Field( ..., description="The validated Polars Python code to run" ) output_file: Optional[str] = Field( None, description="Optional path to save results to" ) # Create tools list tools = [ pydantic_function_tool(ListColumnsArgs), pydantic_function_tool(SampleCSVArgs), pydantic_function_tool(RunTestPolarsCodeArgs), pydantic_function_tool(RunFinalPolarsCodeArgs), ] AGENT_PROMPT = """ You are a world-class expert at crafting precise Polars data transformations in Python. Your goal is to generate accurate code that exactly matches the user's data analysis needs. Use the provided tools to explore the CSV data and construct the perfect Polars transformation. Start by listing columns to understand what's available in the CSV. Sample the CSV to see actual data patterns. Test Polars code with run_test_polars_code before finalizing it. Run the run_test_polars_code tool as many times as needed to get the code working. Only call run_final_polars_code when you're confident the code is perfect. If you find your run_test_polars_code tool call returns an error or won't satisfy the user request, try to fix the code or try a different approach. Think step by step about what information you need. Be sure to specify every parameter for each tool call. Every tool call should have a reasoning parameter which gives you a place to explain why you are calling the tool. When using run_test_polars_code, make sure to test edge cases and validate data types. If saving results to a file, add file writing code to the end of your polars_python_code variable (df.write_csv(output_file)). Your code should use DataFrame to immediately operate on the data. Your polars_python_code variable should be a complete python script that can be run with uv run --with polars. Read the code in the csv_file_path, operate on the data as requested, and print the results. list_columns Returns list of available columns in the CSV file reasoning string Why we need to list columns relative to user request true csv_path string Path to the CSV file true sample_csv Returns sample rows from the CSV file reasoning string Why we need to sample this data true csv_path string Path to the CSV file true row_count integer Number of rows to sample aim for 3-5 rows true run_test_polars_code Tests Polars Python code and returns results (only visible to agent) reasoning string Why we're testing this specific code true polars_python_code string The Complete Polars Python code to test true run_final_polars_code Runs the final validated Polars code and shows results to user reasoning string Final explanation of how code satisfies user request true polars_python_code string The complete validated Polars Python code to run true {{user_request}} {{csv_file_path}} """ def list_columns(reasoning: str, csv_path: str) -> List[str]: """Returns a list of columns in the CSV file. The agent uses this to discover available columns and make informed decisions. This is typically the first tool called to understand the data structure. Args: reasoning: Explanation of why we're listing columns relative to user request csv_path: Path to the CSV file Returns: List of column names as strings Example: columns = list_columns("Need to find age-related columns", "data.csv") # Returns: ['user_id', 'age', 'name', ...] """ try: df = pl.scan_csv(csv_path).collect() columns = df.columns console.log(f"[blue]List Columns Tool[/blue] - Reasoning: {reasoning}") console.log(f"[dim]Columns: {columns}[/dim]") return columns except Exception as e: console.log(f"[red]Error listing columns: {str(e)}[/red]") return [] def sample_csv(reasoning: str, csv_path: str, row_count: int) -> str: """Returns a sample of rows from the CSV file. The agent uses this to understand actual data content and patterns. This helps validate data types and identify any potential data quality issues. Args: reasoning: Explanation of why we're sampling this data csv_path: Path to the CSV file row_count: Number of rows to sample (aim for 3-5 rows) Returns: String containing sample rows in readable format Example: sample = sample_csv("Check age values and formats", "data.csv", 3) # Returns formatted string with 3 rows of data """ try: df = pl.scan_csv(csv_path).limit(row_count).collect() # Convert to string representation output = df.select(pl.all()).write_csv(None) console.log( f"[blue]Sample CSV Tool[/blue] - Rows: {row_count} - Reasoning: {reasoning}" ) console.log(f"[dim]Sample:\n{output}[/dim]") return output except Exception as e: console.log(f"[red]Error sampling CSV: {str(e)}[/red]") return "" def run_test_polars_code(reasoning: str, polars_python_code: str) -> str: """Executes test Polars Python code and returns results. The agent uses this to validate code before finalizing it. Results are only shown to the agent, not the user. The code should use Polars' lazy evaluation (LazyFrame) for better performance. Args: reasoning: Explanation of why we're running this test code polars_python_code: The Polars Python code to test. Should use pl.scan_csv() for lazy evaluation. Returns: Code execution results as a string """ try: # Create a unique filename based on timestamp timestamp = int(time.time()) filename = f"test_polars_{timestamp}.py" # Write code to a real file with open(filename, "w") as f: f.write(polars_python_code) # Execute the code result = subprocess.run( ["uv", "run", "--with", "polars", filename], text=True, capture_output=True, ) output = result.stdout + result.stderr # Clean up the file os.remove(filename) console.log(f"[blue]Test Code Tool[/blue] - Reasoning: {reasoning}") console.log(f"[dim]Code:\n{polars_python_code}[/dim]") return output except Exception as e: console.log(f"[red]Error running test code: {str(e)}[/red]") return str(e) def run_final_polars_code( reasoning: str, polars_python_code: str, ) -> str: """Executes the final Polars code and returns results to user. This is the last tool call the agent should make after validating the code. The code should be fully tested and ready for production use. Results will be displayed to the user and optionally saved to a file. Args: reasoning: Final explanation of how this code satisfies user request polars_python_code: The validated Polars Python code to run. Should use pl.scan_csv() for lazy evaluation. Returns: Code execution results as a string """ try: # Create a unique filename based on timestamp timestamp = int(time.time()) filename = f"polars_code_{timestamp}.py" # Write code to a real file with open(filename, "w") as f: f.write(polars_python_code) # Execute the code result = subprocess.run( ["uv", "run", "--with", "polars", filename], text=True, capture_output=True, ) output = result.stdout + result.stderr # Clean up the file os.remove(filename) console.log(Panel(f"[green]Final Code Tool[/green]\nReasoning: {reasoning}\n")) console.log(f"[dim]Code:\n{polars_python_code}[/dim]") return output except Exception as e: console.log(f"[red]Error running final code: {str(e)}[/red]") return str(e) def main(): # Set up argument parser parser = argparse.ArgumentParser(description="Polars CSV Agent using OpenAI API") parser.add_argument("-i", "--input", required=True, help="Path to input CSV file") parser.add_argument("-p", "--prompt", required=True, help="The user's request") parser.add_argument( "-c", "--compute", type=int, default=10, help="Maximum number of agent loops (default: 10)", ) args = parser.parse_args() # Configure the API key OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") if not OPENAI_API_KEY: console.print( "[red]Error: OPENAI_API_KEY environment variable is not set[/red]" ) console.print( "Please get your API key from https://platform.openai.com/api-keys" ) console.print("Then set it with: export OPENAI_API_KEY='your-api-key-here'") sys.exit(1) openai.api_key = OPENAI_API_KEY # Create a single combined prompt based on the full template completed_prompt = AGENT_PROMPT.replace("{{user_request}}", args.prompt).replace( "{{csv_file_path}}", args.input ) # Initialize messages with proper typing for OpenAI chat messages: List[dict] = [{"role": "user", "content": completed_prompt}] compute_iterations = 0 break_loop = False # Main agent loop while True: if break_loop: break console.rule( f"[yellow]Agent Loop {compute_iterations+1}/{args.compute}[/yellow]" ) compute_iterations += 1 if compute_iterations >= args.compute: console.print( "[yellow]Warning: Reached maximum compute loops without final code[/yellow]" ) console.print( "[yellow]Please try adjusting your prompt or increasing the compute limit.[/yellow]" ) raise Exception( f"Maximum compute loops reached: {compute_iterations}/{args.compute}" ) try: # Generate content with tool support response = openai.chat.completions.create( model="o3-mini", messages=messages, tools=tools, tool_choice="required", ) if response.choices: assert len(response.choices) == 1 message = response.choices[0].message if message.function_call: func_call = message.function_call elif message.tool_calls and len(message.tool_calls) > 0: tool_call = message.tool_calls[0] func_call = tool_call.function else: func_call = None if func_call: func_name = func_call.name func_args_str = func_call.arguments messages.append( { "role": "assistant", "content": None, "tool_calls": [ { "id": tool_call.id, "type": "function", "function": func_call, } ], } ) console.print( f"[blue]Function Call:[/blue] {func_name}({func_args_str})" ) try: # Validate and parse arguments using the corresponding pydantic model if func_name == "ListColumnsArgs": args_parsed = ListColumnsArgs.model_validate_json( func_args_str ) result = list_columns( reasoning=args_parsed.reasoning, csv_path=args_parsed.csv_path, ) elif func_name == "SampleCSVArgs": args_parsed = SampleCSVArgs.model_validate_json( func_args_str ) result = sample_csv( reasoning=args_parsed.reasoning, csv_path=args_parsed.csv_path, row_count=args_parsed.row_count, ) elif func_name == "RunTestPolarsCodeArgs": args_parsed = RunTestPolarsCodeArgs.model_validate_json( func_args_str ) result = run_test_polars_code( reasoning=args_parsed.reasoning, polars_python_code=args_parsed.polars_python_code, ) elif func_name == "RunFinalPolarsCodeArgs": args_parsed = RunFinalPolarsCodeArgs.model_validate_json( func_args_str ) result = run_final_polars_code( reasoning=args_parsed.reasoning, polars_python_code=args_parsed.polars_python_code, ) break_loop = True else: raise Exception(f"Unknown tool call: {func_name}") console.print( f"[blue]Function Call Result:[/blue] {func_name}(...) ->\n{result}" ) # Append the function call result into our messages as a tool response messages.append( { "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps({"result": str(result)}), } ) except Exception as e: error_msg = f"Argument validation failed for {func_name}: {e}" console.print(f"[red]{error_msg}[/red]") messages.append( { "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps({"error": error_msg}), } ) continue else: raise Exception( "No function call in this response - should never happen" ) except Exception as e: console.print(f"[red]Error in agent loop: {str(e)}[/red]") raise e if __name__ == "__main__": main() ================================================ FILE: sfa_scrapper_agent_openai_v2.py ================================================ # /// script # dependencies = [ # "openai>=1.63.0", # "rich>=13.7.0", # "pydantic>=2.0.0", # "firecrawl-py>=0.1.0", # "python-dotenv>=1.0.0", # ] # /// """ Example Usage: uv run sfa_scrapper_agent_openai_v2.py -u "https://example.com" -p "Scrap and format each sentence as a separate line in a markdown list" -o "example.md" uv run sfa_scrapper_agent_openai_v2.py \ --url https://agenticengineer.com/principled-ai-coding \ --prompt "What are the names and descriptions of each lesson?" \ --output-file-path paic-lessons.md \ -c 10 """ import os import sys import json import argparse from typing import List from rich.console import Console from rich.panel import Panel import openai from pydantic import BaseModel, Field from openai import pydantic_function_tool from firecrawl import FirecrawlApp from dotenv import load_dotenv # Load environment variables load_dotenv() # Initialize rich console console = Console() # Initialize Firecrawl FIRECRAWL_API_KEY = os.getenv("FIRECRAWL_API_KEY") if not FIRECRAWL_API_KEY: console.print( "[red]Error: FIRECRAWL_API_KEY not found in environment variables[/red]" ) sys.exit(1) firecrawl_app = FirecrawlApp(api_key=FIRECRAWL_API_KEY) # Initialize OpenAI client client = openai.OpenAI() # Create our list of function tools from our pydantic models class ScrapeUrlArgs(BaseModel): reasoning: str = Field( ..., description="Explanation for why we're scraping this URL" ) url: str = Field(..., description="The URL to scrape") output_file_path: str = Field(..., description="Path to save the scraped content") class ReadLocalFileArgs(BaseModel): reasoning: str = Field( ..., description="Explanation for why we're reading this file" ) file_path: str = Field(..., description="Path of the file to read") class UpdateLocalFileArgs(BaseModel): reasoning: str = Field( ..., description="Explanation for why we're updating this file" ) file_path: str = Field(..., description="Path of the file to update") content: str = Field(..., description="New content to write to the file") class CompleteTaskArgs(BaseModel): reasoning: str = Field(..., description="Explanation of why the task is complete") # Create tools list tools = [ pydantic_function_tool(ScrapeUrlArgs), pydantic_function_tool(ReadLocalFileArgs), pydantic_function_tool(UpdateLocalFileArgs), pydantic_function_tool(CompleteTaskArgs), ] AGENT_PROMPT = """ You are a world-class web scraping and content filtering expert. Your goal is to scrape web content and filter it according to the user's needs. Run scrap_url, then read_local_file, then update_local_file as many times as needed to satisfy the user's prompt, then complete_task when the user's prompt is fully satisfied. When processing content, extract exactly what the user asked for - no more, no less. When saving processed content, use proper markdown formatting. Use tools available in 'tools' section. scrape_url Scrapes content from a URL and saves it to a file reasoning string Why we need to scrape this URL true url string The URL to scrape true output_file_path string Where to save the scraped content true read_local_file Reads content from a local file reasoning string Why we need to read this file true file_path string Path of file to read true update_local_file Updates content in a local file reasoning string Why we need to update this file true file_path string Path of file to update true content string New content to write to the file true complete_task Signals that the task is complete reasoning string Why the task is now complete true {{user_prompt}} {{url}} {{output_file_path}} """ def log_function_call(function_name: str, function_args: dict): """Log a function call in a rich panel.""" args_str = ", ".join(f"{k}={repr(v)}" for k, v in function_args.items()) console.print( Panel( f"{function_name}({args_str})", title="[blue]Function Call[/blue]", border_style="blue", ) ) def log_function_result(function_name: str, result: str): """Log a function result in a rich panel.""" console.print( Panel( str(result), title=f"[green]{function_name} Result[/green]", border_style="green", ) ) def log_error(error_msg: str): """Log an error in a rich panel.""" console.print(Panel(str(error_msg), title="[red]Error[/red]", border_style="red")) def scrape_url(reasoning: str, url: str, output_file_path: str) -> str: """Scrapes content from a URL and saves it to a file.""" log_function_call( "scrape_url", {"reasoning": reasoning, "url": url, "output_file_path": output_file_path}, ) try: response = firecrawl_app.scrape_url( url=url, params={ "formats": ["markdown"], }, ) if response.get("markdown"): content = response["markdown"] with open(output_file_path, "w") as f: f.write(content) log_function_result( "scrape_url", f"Successfully scraped {len(content)} characters" ) return content else: error = response.get("error", "Unknown error") log_error(f"Error scraping URL: {error}") return "" except Exception as e: log_error(f"Error scraping URL: {str(e)}") return "" def read_local_file(reasoning: str, file_path: str) -> str: """Reads content from a local file. Args: reasoning: Explanation for why we're reading this file file_path: Path of the file to read Returns: String containing the file contents """ log_function_call( "read_local_file", {"reasoning": reasoning, "file_path": file_path} ) try: console.log( f"[blue]Reading File[/blue] - File: {file_path} - Reasoning: {reasoning}" ) with open(file_path, "r") as f: return f.read() except Exception as e: console.log(f"[red]Error reading file: {str(e)}[/red]") return "" def update_local_file(reasoning: str, file_path: str, content: str) -> str: """Updates content in a local file. Args: reasoning: Explanation for why we're updating this file file_path: Path of the file to update content: New content to write to the file Returns: String indicating success or failure """ log_function_call( "update_local_file", { "reasoning": reasoning, "file_path": file_path, "content": f"{len(content)} characters", # Don't log full content }, ) try: console.log( f"[blue]Updating File[/blue] - File: {file_path} - Reasoning: {reasoning}" ) with open(file_path, "w") as f: f.write(content) log_function_result( "update_local_file", f"Successfully wrote {len(content)} characters" ) return "File updated successfully" except Exception as e: console.log(f"[red]Error updating file: {str(e)}[/red]") return f"Error: {str(e)}" def complete_task(reasoning: str) -> str: """Signals that the task is complete. Args: reasoning: Explanation of why the task is complete Returns: String confirmation message """ log_function_call("complete_task", {"reasoning": reasoning}) console.log(f"[green]Task Complete[/green] - Reasoning: {reasoning}") result = "Task completed successfully" log_function_result("complete_task", result) return result def main(): # Set up argument parser parser = argparse.ArgumentParser( description="Web scraper agent that filters content based on user query" ) parser.add_argument("--url", "-u", required=True, help="The URL to scrape") parser.add_argument( "--output-file-path", "-o", default="scraped_content.md", help="Path to save the scraped content", ) parser.add_argument( "--prompt", "-p", required=True, help="The prompt to filter the content with" ) parser.add_argument( "--compute-limit", "-c", type=int, default=10, # Increased default compute limit help="Maximum number of tokens to use for response", ) args = parser.parse_args() # Format the prompt with the user's arguments formatted_prompt = ( AGENT_PROMPT.replace("{{user_prompt}}", args.prompt) .replace("{{url}}", args.url) .replace("{{output_file_path}}", args.output_file_path) ) # Initialize conversation with system prompt and workflow start messages = [ { "role": "user", "content": formatted_prompt, }, ] # Track number of iterations iterations = 0 max_iterations = args.compute_limit break_loop = False while iterations < max_iterations: if break_loop: break iterations += 1 try: console.rule(f"[yellow]Agent Loop {iterations}/{max_iterations}[/yellow]") # Get completion from OpenAI completion = client.chat.completions.create( model="o3-mini", messages=messages, tools=tools, tool_choice="auto", ) response_message = completion.choices[0].message # Print the assistant's response assistant_content = response_message.content or "" if assistant_content: console.print(Panel(assistant_content, title="Assistant")) messages.append( { "role": "assistant", "content": assistant_content, } ) # Handle tool calls if response_message.tool_calls: # Add assistant's message to conversation messages.append( { "role": "assistant", "content": response_message.content, "tool_calls": [ { "id": tool_call.id, "type": tool_call.type, "function": { "name": tool_call.function.name, "arguments": tool_call.function.arguments, }, } for tool_call in response_message.tool_calls ], } ) # Process each tool call for tool_call in response_message.tool_calls: function_name = tool_call.function.name function_args = json.loads(tool_call.function.arguments) console.print( Panel( f"Processing tool call: {function_name}({function_args})", title="[yellow]Tool Call[/yellow]", border_style="yellow", ) ) # Execute the appropriate function and store result result = None try: if function_name == "ScrapeUrlArgs": result = scrape_url(**function_args) elif function_name == "ReadLocalFileArgs": result = read_local_file(**function_args) elif function_name == "UpdateLocalFileArgs": result = update_local_file(**function_args) elif function_name == "CompleteTaskArgs": result = complete_task(**function_args) break_loop = True else: raise ValueError(f"Unknown function: {function_name}") except Exception as e: error_msg = f"Error executing {function_name}: {str(e)}" console.print(Panel(error_msg, title="[red]Error[/red]")) result = f"Error executing {function_name}({function_args}): {str(e)}" # Add the tool response to messages messages.append( { "role": "tool", "tool_call_id": tool_call.id, "name": function_name, "content": str(result), } ) else: raise ValueError("No tool calls found - should not happen") except Exception as e: log_error(f"Error: {str(e)}") console.print("[yellow]Messages at error:[/yellow]") if iterations >= max_iterations: log_error("Reached maximum number of iterations") raise Exception("Reached maximum number of iterations") if __name__ == "__main__": main() ================================================ FILE: sfa_sqlite_openai_v2.py ================================================ # /// script # dependencies = [ # "openai>=1.63.0", # "rich>=13.7.0", # "pydantic>=2.0.0", # ] # /// import os import sys import json import argparse import sqlite3 import subprocess from typing import List from rich.console import Console from rich.panel import Panel import openai from pydantic import BaseModel, Field, ValidationError from openai import pydantic_function_tool # Initialize rich console console = Console() # Create our list of function tools from our pydantic models class ListTablesArgs(BaseModel): reasoning: str = Field( ..., description="Explanation for listing tables relative to the user request" ) class DescribeTableArgs(BaseModel): reasoning: str = Field(..., description="Reason why the table schema is needed") table_name: str = Field(..., description="Name of the table to describe") class SampleTableArgs(BaseModel): reasoning: str = Field(..., description="Explanation for sampling the table") table_name: str = Field(..., description="Name of the table to sample") row_sample_size: int = Field( ..., description="Number of rows to sample (aim for 3-5 rows)" ) class RunTestSQLQuery(BaseModel): reasoning: str = Field(..., description="Reason for testing this query") sql_query: str = Field(..., description="The SQL query to test") class RunFinalSQLQuery(BaseModel): reasoning: str = Field( ..., description="Final explanation of how this query satisfies the user request", ) sql_query: str = Field(..., description="The validated SQL query to run") # Create tools list tools = [ pydantic_function_tool(ListTablesArgs), pydantic_function_tool(DescribeTableArgs), pydantic_function_tool(SampleTableArgs), pydantic_function_tool(RunTestSQLQuery), pydantic_function_tool(RunFinalSQLQuery), ] AGENT_PROMPT = """ You are a world-class expert at crafting precise SQLite SQL queries. Your goal is to generate accurate queries that exactly match the user's data needs. Use the provided tools to explore the database and construct the perfect query. Start by listing tables to understand what's available. Describe tables to understand their schema and columns. Sample tables to see actual data patterns. Test queries before finalizing them. Only call run_final_sql_query when you're confident the query is perfect. Be thorough but efficient with tool usage. If you find your run_test_sql_query tool call returns an error or won't satisfy the user request, try to fix the query or try a different query. Think step by step about what information you need. Be sure to specify every parameter for each tool call. Every tool call should have a reasoning parameter which gives you a place to explain why you are calling the tool. list_tables Returns list of available tables in database reasoning string Why we need to list tables relative to user request true describe_table Returns schema info for specified table reasoning string Why we need to describe this table true table_name string Name of table to describe true sample_table Returns sample rows from specified table, always specify row_sample_size reasoning string Why we need to sample this table true table_name string Name of table to sample true row_sample_size integer Number of rows to sample aim for 3-5 rows true run_test_sql_query Tests a SQL query and returns results (only visible to agent) reasoning string Why we're testing this specific query true sql_query string The SQL query to test true run_final_sql_query Runs the final validated SQL query and shows results to user reasoning string Final explanation of how query satisfies user request true sql_query string The validated SQL query to run true {{user_request}} """ def list_tables(reasoning: str) -> List[str]: """Returns a list of tables in the database. The agent uses this to discover available tables and make informed decisions. Args: reasoning: Explanation of why we're listing tables relative to user request Returns: List of table names as strings """ try: conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';") tables = [row[0] for row in cursor.fetchall()] conn.close() console.log(f"[blue]List Tables Tool[/blue] - Reasoning: {reasoning}") return tables except Exception as e: console.log(f"[red]Error listing tables: {str(e)}[/red]") return [] def describe_table(reasoning: str, table_name: str) -> str: """Returns schema information about the specified table. The agent uses this to understand table structure and available columns. Args: reasoning: Explanation of why we're describing this table table_name: Name of table to describe Returns: String containing table schema information """ try: conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute(f"PRAGMA table_info('{table_name}');") rows = cursor.fetchall() conn.close() output = "\n".join([str(row) for row in rows]) console.log(f"[blue]Describe Table Tool[/blue] - Table: {table_name} - Reasoning: {reasoning}") return output except Exception as e: console.log(f"[red]Error describing table: {str(e)}[/red]") return "" def sample_table(reasoning: str, table_name: str, row_sample_size: int) -> str: """Returns a sample of rows from the specified table. The agent uses this to understand actual data content and patterns. Args: reasoning: Explanation of why we're sampling this table table_name: Name of table to sample from row_sample_size: Number of rows to sample aim for 3-5 rows Returns: String containing sample rows in readable format """ try: conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute(f"SELECT * FROM {table_name} LIMIT {row_sample_size};") rows = cursor.fetchall() conn.close() output = "\n".join([str(row) for row in rows]) console.log( f"[blue]Sample Table Tool[/blue] - Table: {table_name} - Rows: {row_sample_size} - Reasoning: {reasoning}" ) return output except Exception as e: console.log(f"[red]Error sampling table: {str(e)}[/red]") return "" def run_test_sql_query(reasoning: str, sql_query: str) -> str: """Executes a test SQL query and returns results. The agent uses this to validate queries before finalizing them. Results are only shown to the agent, not the user. Args: reasoning: Explanation of why we're running this test query sql_query: The SQL query to test Returns: Query results as a string """ try: conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute(sql_query) rows = cursor.fetchall() conn.commit() conn.close() output = "\n".join([str(row) for row in rows]) console.log(f"[blue]Test Query Tool[/blue] - Reasoning: {reasoning}") console.log(f"[dim]Query: {sql_query}[/dim]") return output except Exception as e: console.log(f"[red]Error running test query: {str(e)}[/red]") return str(e) def run_final_sql_query(reasoning: str, sql_query: str) -> str: """Executes the final SQL query and returns results to user. This is the last tool call the agent should make after validating the query. Args: reasoning: Final explanation of how this query satisfies user request sql_query: The validated SQL query to run Returns: Query results as a string """ try: conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute(sql_query) rows = cursor.fetchall() conn.commit() conn.close() output = "\n".join([str(row) for row in rows]) console.log( Panel( f"[green]Final Query Tool[/green]\nReasoning: {reasoning}\nQuery: {sql_query}" ) ) return output except Exception as e: console.log(f"[red]Error running final query: {str(e)}[/red]") return str(e) def main(): # Set up argument parser parser = argparse.ArgumentParser(description="SQLite Agent using OpenAI API") parser.add_argument( "-d", "--db", required=True, help="Path to SQLite database file" ) parser.add_argument("-p", "--prompt", required=True, help="The user's request") parser.add_argument( "-c", "--compute", type=int, default=10, help="Maximum number of agent loops (default: 3)", ) args = parser.parse_args() # Configure the API key OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") if not OPENAI_API_KEY: console.print( "[red]Error: OPENAI_API_KEY environment variable is not set[/red]" ) console.print( "Please get your API key from https://platform.openai.com/api-keys" ) console.print("Then set it with: export OPENAI_API_KEY='your-api-key-here'") sys.exit(1) openai.api_key = OPENAI_API_KEY # Set global DB_PATH for tool functions global DB_PATH DB_PATH = args.db # Create a single combined prompt based on the full template completed_prompt = AGENT_PROMPT.replace("{{user_request}}", args.prompt) messages = [{"role": "user", "content": completed_prompt}] compute_iterations = 0 # Main agent loop while True: console.rule( f"[yellow]Agent Loop {compute_iterations+1}/{args.compute}[/yellow]" ) compute_iterations += 1 if compute_iterations >= args.compute: console.print( "[yellow]Warning: Reached maximum compute loops without final query[/yellow]" ) raise Exception( f"Maximum compute loops reached: {compute_iterations}/{args.compute}" ) try: # Generate content with tool support response = openai.chat.completions.create( model="o3-mini", # model="gpt-4o-mini", messages=messages, tools=tools, tool_choice="required", ) if response.choices: assert len(response.choices) == 1 message = response.choices[0].message if message.function_call: func_call = message.function_call elif message.tool_calls and len(message.tool_calls) > 0: # If a tool_calls list is present, use the first call and extract its function details. tool_call = message.tool_calls[0] func_call = tool_call.function else: func_call = None if func_call: func_name = func_call.name func_args_str = func_call.arguments messages.append( { "role": "assistant", "tool_calls": [ { "id": tool_call.id, "type": "function", "function": func_call, } ], } ) console.print( f"[blue]Function Call:[/blue] {func_name}({func_args_str})" ) try: # Validate and parse arguments using the corresponding pydantic model if func_name == "ListTablesArgs": args_parsed = ListTablesArgs.model_validate_json( func_args_str ) result = list_tables(reasoning=args_parsed.reasoning) elif func_name == "DescribeTableArgs": args_parsed = DescribeTableArgs.model_validate_json( func_args_str ) result = describe_table( reasoning=args_parsed.reasoning, table_name=args_parsed.table_name, ) elif func_name == "SampleTableArgs": args_parsed = SampleTableArgs.model_validate_json( func_args_str ) result = sample_table( reasoning=args_parsed.reasoning, table_name=args_parsed.table_name, row_sample_size=args_parsed.row_sample_size, ) elif func_name == "RunTestSQLQuery": args_parsed = RunTestSQLQuery.model_validate_json( func_args_str ) result = run_test_sql_query( reasoning=args_parsed.reasoning, sql_query=args_parsed.sql_query, ) elif func_name == "RunFinalSQLQuery": args_parsed = RunFinalSQLQuery.model_validate_json( func_args_str ) result = run_final_sql_query( reasoning=args_parsed.reasoning, sql_query=args_parsed.sql_query, ) console.print("\n[green]Final Results:[/green]") console.print(result) return else: raise Exception(f"Unknown tool call: {func_name}") console.print( f"[blue]Function Call Result:[/blue] {func_name}(...) ->\n{result}" ) # Append the function call result into our messages as a tool response messages.append( { "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps({"result": str(result)}), } ) except Exception as e: error_msg = f"Argument validation failed for {func_name}: {e}" console.print(f"[red]{error_msg}[/red]") messages.append( { "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps({"error": error_msg}), } ) continue else: raise Exception( "No function call in this response - should never happen" ) except Exception as e: console.print(f"[red]Error in agent loop: {str(e)}[/red]") raise e if __name__ == "__main__": main()