[
  {
    "path": ".github/pull_request_template.md",
    "content": "## Description\n<!-- Describe the changes you've made -->\n\n## Quickstart\n- [ ] Computer Use Demo\n- [ ] Customer Support Agent\n- [ ] Financial Data Analyst\n- [ ] N/A\n\n## Type of Change\n- [ ] Bug fix\n- [ ] New feature\n- [ ] Documentation update\n- [ ] Code refactoring\n- [ ] Other (please describe):\n\n## Testing\n<!-- Describe the testing you've done -->\n- [ ] Added/updated unit tests\n- [ ] Tested manually\n- [ ] Verified in development environment\n\n## Screenshots\n<!-- If applicable, add screenshots to help explain your changes -->\n\n## Additional Notes\n<!-- Add any additional context or notes about the changes -->"
  },
  {
    "path": ".github/workflows/build.yaml",
    "content": "env:\n  REGISTRY: ghcr.io/anthropics/anthropic-quickstarts\nname: build\non:\n  pull_request:\n    paths:\n      - .github/**\n      - computer-use-demo/**\n  push:\n    branches:\n      - main\n    paths:\n      - .github/**\n      - computer-use-demo/**\njobs:\n  build-amd64:\n    uses: ./.github/workflows/reusable_build_step.yaml\n    with:\n      platform: amd64\n      builder: ubuntu-latest-16core\n      registry: ghcr.io/anthropics/anthropic-quickstarts\n      tag_prefix: computer-use-demo-\n      context: computer-use-demo\n    permissions:\n      contents: read\n      packages: write\n  build-arm64:\n    uses: ./.github/workflows/reusable_build_step.yaml\n    with:\n      platform: arm64\n      builder: ubuntu-22.04-arm64-16core\n      registry: ghcr.io/anthropics/anthropic-quickstarts\n      tag_prefix: computer-use-demo-\n      context: computer-use-demo\n    permissions:\n      contents: read\n      packages: write\n  merge:\n    runs-on: ubuntu-latest\n    needs:\n      - build-arm64\n      - build-amd64\n    permissions:\n      contents: read\n      packages: write\n    steps:\n      - uses: actions/checkout@v4\n      - name: Login to ghcr.io\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{github.actor}}\n          password: ${{secrets.GITHUB_TOKEN}}\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n      - name: Set image tag\n        run: |\n          echo \"SHORT_SHA=$(git rev-parse --short ${{ github.sha }})\" >> \"$GITHUB_ENV\"\n      - name: Create SHA manifest and push\n        run: |\n          docker buildx imagetools create -t \\\n            ${REGISTRY}:computer-use-demo-${SHORT_SHA} \\\n            ${REGISTRY}:computer-use-demo-${SHORT_SHA}-amd64 \\\n            ${REGISTRY}:computer-use-demo-${SHORT_SHA}-arm64\n\n      - name: Create latest manifest and push\n        if: github.event_name == 'push' && github.ref == 'refs/heads/main'\n        run: |\n          docker buildx imagetools create -t \\\n            ${REGISTRY}:computer-use-demo-latest \\\n            ${REGISTRY}:computer-use-demo-latest-amd64 \\\n            ${REGISTRY}:computer-use-demo-latest-arm64\n"
  },
  {
    "path": ".github/workflows/reusable_build_step.yaml",
    "content": "on:\n  workflow_call:\n    inputs:\n      platform:\n        required: true\n        type: string\n      builder:\n        required: true\n        type: string\n      registry:\n        required: true\n        type: string\n      tag_prefix:\n        required: false\n        type: string\n      context:\n        required: false\n        type: string\njobs:\n  build:\n    runs-on: ${{ inputs.builder }}\n    permissions:\n      contents: read\n      packages: write\n    steps:\n      - uses: actions/checkout@v4\n      - name: Login to ghcr.io\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{github.actor}}\n          password: ${{secrets.GITHUB_TOKEN}}\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n      - name: Set image tag\n        run: |\n          short_sha=$(git rev-parse --short ${{ github.sha }})\n          echo \"TAG=${{ inputs.registry }}:${{ inputs.tag_prefix }}${short_sha}\" >> \"$GITHUB_ENV\"\n      - name: Build Docker image\n        uses: docker/build-push-action@v5\n        with:\n          platforms: linux/${{ inputs.platform }}\n          context: ${{ inputs.context || '.' }}\n          push: false\n          tags: ${{ env.TAG }}\n          cache-from: type=gha,scope=computer-use-${{ inputs.platform }}\n          cache-to: type=gha,mode=max,scope=computer-use-${{ inputs.platform }}\n          load: true\n      - name: Run container\n        run: docker run -d -p 8051:8051 ${{ env.TAG }}\n      - name: Check streamlit\n        run: |\n          timeout=60\n          start_time=$(date +%s)\n          docker_id=$(docker ps --filter \"ancestor=${{ env.TAG }}\" --format \"{{.ID}}\")\n          echo \"docker_id=$docker_id\" >> \"$GITHUB_ENV\"\n          while true; do\n            current_time=$(date +%s)\n            elapsed=$((current_time - start_time))\n            if [ $elapsed -ge $timeout ]; then\n              echo \"Timeout reached. Container did not respond within $timeout seconds.\"\n              exit 1\n            fi\n            response=$(docker exec $docker_id curl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:8501 || echo \"000\")\n            if [ \"$response\" = \"200\" ]; then\n              echo \"Container responded with 200 OK\"\n              exit 0\n            fi\n          done\n      - name: Check VNC\n        run: docker exec $docker_id nc localhost 5900 -z\n      - name: Check noVNC\n        run: docker exec $docker_id curl -s -o /dev/null -w \"%{http_code}\" http://localhost:6080 | grep -q 200 || exit 1\n      - name: Check landing page\n        run: docker exec $docker_id curl -s -o /dev/null -w \"%{http_code}\" http://localhost:8080 | grep -q 200 || exit 1\n      - name: Determine push tags\n        run: |\n          if [ \"${{ github.event_name }}\" == \"pull_request\" ]; then\n            echo \"PUSH_TAGS=${TAG}-${{ inputs.platform }}\" >> \"$GITHUB_ENV\"\n          else\n            echo \"PUSH_TAGS=${TAG}-${{ inputs.platform }},${{ inputs.registry }}:${{ inputs.tag_prefix }}latest-${{ inputs.platform }}\" >> \"$GITHUB_ENV\"\n          fi\n      - name: Push Docker image\n        uses: docker/build-push-action@v5\n        with:\n          platforms: linux/${{ inputs.platform }}\n          context: ${{ inputs.context || '.' }}\n          push: true\n          tags: ${{ env.PUSH_TAGS }}\n          cache-from: type=gha,scope=computer-use-${{ inputs.platform }}\n          cache-to: type=gha,mode=max,scope=computer-use-${{ inputs.platform }}\n"
  },
  {
    "path": ".github/workflows/tests.yaml",
    "content": "name: tests\non:\n  pull_request:\n    paths:\n      - .github/**\n      - computer-use-demo/**\n  push:\n    branches:\n      - main\n    paths:\n      - .github/**\n      - computer-use-demo/**\njobs:\n  ruff:\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: computer-use-demo\n    steps:\n      - uses: actions/checkout@v4\n      - uses: astral-sh/ruff-action@v1\n        with:\n          src: \"computer-use-demo\"\n  pyright:\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: computer-use-demo\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-python@v5\n        with:\n          cache: \"pip\"\n          python-version: \"3.11.6\"\n      - run: |\n          python -m venv .venv\n          source .venv/bin/activate\n          pip install -r dev-requirements.txt\n      - run: echo \"$PWD/.venv/bin\" >> $GITHUB_PATH\n      - uses: jakebailey/pyright-action@v1\n        with:\n          working-directory: computer-use-demo\n  pytest:\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: computer-use-demo\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-python@v5\n        with:\n          cache: \"pip\"\n          python-version: \"3.11.6\"\n      - run: |\n          python -m venv .venv\n          source .venv/bin/activate\n          pip install -r dev-requirements.txt\n      - run: echo \"$PWD/.venv/bin\" >> $GITHUB_PATH\n      - run: pytest tests --junitxml=junit/test-results.xml\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "files: ^computer-use-demo/\ndefault_stages: [pre-commit, pre-push]\nrepos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v2.3.0\n    hooks:\n      - id: check-yaml\n      - id: end-of-file-fixer\n      - id: trailing-whitespace\n  - repo: https://github.com/astral-sh/ruff-pre-commit\n    rev: v0.6.7\n    hooks:\n      - id: ruff\n        name: Run `ruff` to autofix lint errors\n        args: [--fix-only]\n      - id: ruff\n        name: Run `ruff` to format code\n        entry: ruff format --force-exclude\n      - id: ruff\n        name: Run `ruff` to lint code\n  - repo: https://github.com/RobertCraigie/pyright-python\n    rev: v1.1.384\n    hooks:\n      - id: pyright\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# Claude Quickstarts Development Guide\n\n## Legal\n\n- When changes are made to files that have a copyright notice add them to that subdirectory's CHANGELOG.md file.\n\n## Computer-Use Demo\n\n### Setup & Development\n\n- **Setup environment**: `./setup.sh`\n- **Build Docker**: `docker build . -t computer-use-demo:local`\n- **Run container**: `docker run -e ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY -v $(pwd)/computer_use_demo:/home/computeruse/computer_use_demo/ -v $HOME/.anthropic:/home/computeruse/.anthropic -p 5900:5900 -p 8501:8501 -p 6080:6080 -p 8080:8080 -it computer-use-demo:local`\n\n### Testing & Code Quality\n\n- **Lint**: `ruff check .`\n- **Format**: `ruff format .`\n- **Typecheck**: `pyright`\n- **Run tests**: `pytest`\n- **Run single test**: `pytest tests/path_to_test.py::test_name -v`\n\n### Code Style\n\n- **Python**: snake_case for functions/variables, PascalCase for classes\n- **Imports**: Use isort with combine-as-imports\n- **Error handling**: Use custom ToolError for tool errors\n- **Types**: Add type annotations for all parameters and returns\n- **Classes**: Use dataclasses and abstract base classes\n\n## Customer Support Agent\n\n### Setup & Development\n\n- **Install dependencies**: `npm install`\n- **Run dev server**: `npm run dev` (full UI)\n- **UI variants**: `npm run dev:left` (left sidebar), `npm run dev:right` (right sidebar), `npm run dev:chat` (chat only)\n- **Lint**: `npm run lint`\n- **Build**: `npm run build` (full UI), see package.json for variants\n\n### Code Style\n\n- **TypeScript**: Strict mode with proper interfaces\n- **Components**: Function components with React hooks\n- **Formatting**: Follow ESLint Next.js configuration\n- **UI components**: Use shadcn/ui components library\n\n## Financial Data Analyst\n\n### Setup & Development\n\n- **Install dependencies**: `npm install`\n- **Run dev server**: `npm run dev`\n- **Lint**: `npm run lint`\n- **Build**: `npm run build`\n\n### Code Style\n\n- **TypeScript**: Strict mode with proper type definitions\n- **Components**: Function components with type annotations\n- **Visualization**: Use Recharts library for data visualization\n- **State management**: React hooks for state"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Anthropic\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README.md",
    "content": "# Claude Quickstarts\n\nClaude Quickstarts is a collection of projects designed to help developers quickly get started with building  applications using the Claude API. Each quickstart provides a foundation that you can easily build upon and customize for your specific needs.\n\n## Getting Started\n\nTo use these quickstarts, you'll need an Claude API key. If you don't have one yet, you can sign up for free at [console.anthropic.com](https://console.anthropic.com).\n\n## Available Quickstarts\n\n### Customer Support Agent\n\nA customer support agent powered by Claude. This project demonstrates how to leverage Claude's natural language understanding and generation capabilities to create an AI-assisted customer support system with access to a knowledge base.\n\n[Go to Customer Support Agent Quickstart](./customer-support-agent)\n\n### Financial Data Analyst\n\nA financial data analyst powered by Claude. This project demonstrates how to leverage Claude's capabilities with interactive data visualization to analyze financial data via chat.\n\n[Go to Financial Data Analyst Quickstart](./financial-data-analyst)\n\n### Computer Use Demo\n\nAn environment and tools that Claude can use to control a desktop computer. This project demonstrates how to leverage the computer use capabilities of Claude, including support for the latest `computer_use_20251124` tool version with zoom actions.\n\n[Go to Computer Use Demo Quickstart](./computer-use-demo)\n\n### Browser Tools API Demo\n\nA complete reference implementation for browser automation powered by Claude. This project demonstrates how to leverage Claude's browser tools API for web interaction, including navigation, DOM inspection, and form manipulation using Playwright.\n\n[Go to Browser Tools API Demo Quickstart](./browser-tools-api-demo)\n\n### Autonomous Coding Agent\n\nAn autonomous coding agent powered by the Claude Agent SDK. This project demonstrates a two-agent pattern (initializer + coding agent) that can build complete applications over multiple sessions, with progress persisted via git and a feature list that the agent works through incrementally.\n\n[Go to Autonomous Coding Agent Quickstart](./autonomous-coding)\n\n## General Usage\n\nEach quickstart project comes with its own README and setup instructions. Generally, you'll follow these steps:\n\n1. Clone this repository\n2. Navigate to the specific quickstart directory\n3. Install the required dependencies\n4. Set up your Claude API key as an environment variable\n5. Run the quickstart application\n\n## Explore Further\n\nTo deepen your understanding of working with Claude and the Claude API, check out these resources:\n\n- [Claude API Documentation](https://docs.claude.com)\n- [Claude Cookbooks](https://github.com/anthropics/claude-cookbooks) - A collection of code snippets and guides for common tasks\n- [Claude API Fundamentals Course](https://github.com/anthropics/courses/tree/master/anthropic_api_fundamentals)\n\n## Contributing\n\nWe welcome contributions to the Claude Quickstarts repository! If you have ideas for new quickstart projects or improvements to existing ones, please open an issue or submit a pull request.\n\n## Community and Support\n\n- Join our [Anthropic Discord community](https://www.anthropic.com/discord) for discussions and support\n- Check out the [Anthropic support documentation](https://support.anthropic.com) for additional help\n\n## License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.\n"
  },
  {
    "path": "agents/.gitignore",
    "content": "# Python bytecode\n__pycache__/\n*.py[cod]\n*$py.class\n\n# Jupyter Notebook\n.ipynb_checkpoints\n*/.ipynb_checkpoints/*\n\n# MacOS\n.DS_Store\n\n# Environment variables\n.env\n\n# Virtual environments\nvenv/\nenv/\n.venv/\n\n# MCP logs\n*.log\n\n# Distribution / packaging\ndist/\nbuild/\n*.egg-info/"
  },
  {
    "path": "agents/README.md",
    "content": "# Agents\n\nA minimal educational implementation of LLM agents using the Claude API.\n\n> **Note:** This is NOT an SDK, but a reference implementation of key concepts\n\n## Overview & Core Components\n\nThis repo demonstrates how to [build effective agents](https://www.anthropic.com/engineering/building-effective-agents) with the Claude API. It shows how sophisticated AI behaviors can emerge from a simple foundation: LLMs using tools in a loop. This implementation is not prescriptive - the core logic is <300 lines of code and deliberately lacks production features. Feel free to translate these patterns to your language and production stack ([Claude Code](https://docs.claude.com/en/docs/agents-and-tools/claude-code/overview) can help!)\n\nIt contains three components:\n\n- `agent.py`: Manages Claude API interactions and tool execution\n- `tools/`: Tool implementations (both native and MCP tools)\n- `utils/`: Utilities for message history and MCP server connections\n\n## Usage\n\n```python\nfrom agents.agent import Agent\nfrom agents.tools.think import ThinkTool\n\n# Create an agent with both local tools and MCP server tools\nagent = Agent(\n    name=\"MyAgent\",\n    system=\"You are a helpful assistant.\",\n    tools=[ThinkTool()],  # Local tools\n    mcp_servers=[\n        {\n            \"type\": \"stdio\",\n            \"command\": \"python\",\n            \"args\": [\"-m\", \"mcp_server\"],\n        },\n    ]\n)\n\n# Run the agent\nresponse = agent.run(\"What should I consider when buying a new laptop?\")\n```\n\nFrom this foundation, you can add domain-specific tools, optimize performance, or implement custom response handling. We remain deliberately unopinionated - this backbone simply gets you started with fundamentals.\n\n## Requirements\n\n- Python 3.8+\n- Claude API key (set as `ANTHROPIC_API_KEY` environment variable)\n- `anthropic` Python library\n- `mcp` Python library"
  },
  {
    "path": "agents/__init__.py",
    "content": "\"\"\"Core agent implementations.\"\"\"\n\nfrom .agent import Agent, ModelConfig\nfrom .tools.base import Tool\n\n__all__ = [\"Agent\", \"ModelConfig\", \"Tool\"]\n"
  },
  {
    "path": "agents/agent.py",
    "content": "\"\"\"Agent implementation with Claude API and tools.\"\"\"\n\nimport asyncio\nimport os\nfrom contextlib import AsyncExitStack\nfrom dataclasses import dataclass\nfrom typing import Any\n\nfrom anthropic import Anthropic\n\nfrom .tools.base import Tool\nfrom .utils.connections import setup_mcp_connections\nfrom .utils.history_util import MessageHistory\nfrom .utils.tool_util import execute_tools\n\n\n@dataclass\nclass ModelConfig:\n    \"\"\"Configuration settings for Claude model parameters.\"\"\"\n\n    # Available models include:\n    # - claude-sonnet-4-20250514 (default)\n    # - claude-opus-4-20250514\n    # - claude-haiku-4-5-20251001\n    # - claude-3-5-sonnet-20240620\n    # - claude-3-haiku-20240307\n    model: str = \"claude-sonnet-4-20250514\"\n    max_tokens: int = 4096\n    temperature: float = 1.0\n    context_window_tokens: int = 180000\n\n\nclass Agent:\n    \"\"\"Claude-powered agent with tool use capabilities.\"\"\"\n\n    def __init__(\n        self,\n        name: str,\n        system: str,\n        tools: list[Tool] | None = None,\n        mcp_servers: list[dict[str, Any]] | None = None,\n        config: ModelConfig | None = None,\n        verbose: bool = False,\n        client: Anthropic | None = None,\n        message_params: dict[str, Any] | None = None,\n    ):\n        \"\"\"Initialize an Agent.\n        \n        Args:\n            name: Agent identifier for logging\n            system: System prompt for the agent\n            tools: List of tools available to the agent\n            mcp_servers: MCP server configurations\n            config: Model configuration with defaults\n            verbose: Enable detailed logging\n            client: Anthropic client instance\n            message_params: Additional parameters for client.messages.create().\n                           These override any conflicting parameters from config.\n        \"\"\"\n        self.name = name\n        self.system = system\n        self.verbose = verbose\n        self.tools = list(tools or [])\n        self.config = config or ModelConfig()\n        self.mcp_servers = mcp_servers or []\n        self.message_params = message_params or {}\n        self.client = client or Anthropic(\n            api_key=os.environ.get(\"ANTHROPIC_API_KEY\", \"\")\n        )\n        self.history = MessageHistory(\n            model=self.config.model,\n            system=self.system,\n            context_window_tokens=self.config.context_window_tokens,\n            client=self.client,\n        )\n\n        if self.verbose:\n            print(f\"\\n[{self.name}] Agent initialized\")\n\n    def _prepare_message_params(self) -> dict[str, Any]:\n        \"\"\"Prepare parameters for client.messages.create() call.\n        \n        Returns a dict with base parameters from config, with any\n        message_params overriding conflicting keys.\n        \"\"\"\n        return {\n            \"model\": self.config.model,\n            \"max_tokens\": self.config.max_tokens,\n            \"temperature\": self.config.temperature,\n            \"system\": self.system,\n            \"messages\": self.history.format_for_api(),\n            \"tools\": [tool.to_dict() for tool in self.tools],\n            **self.message_params,\n        }\n\n    async def _agent_loop(self, user_input: str) -> list[dict[str, Any]]:\n        \"\"\"Process user input and handle tool calls in a loop\"\"\"\n        if self.verbose:\n            print(f\"\\n[{self.name}] Received: {user_input}\")\n        await self.history.add_message(\"user\", user_input, None)\n\n        tool_dict = {tool.name: tool for tool in self.tools}\n\n        while True:\n            self.history.truncate()\n            params = self._prepare_message_params()\n\n            # Merge headers properly - default beta header can be overridden by message_params\n            default_headers = {\"anthropic-beta\": \"code-execution-2025-05-22\"}\n            if \"extra_headers\" in params:\n                # Pop extra_headers from params and merge with defaults\n                custom_headers = params.pop(\"extra_headers\")\n                merged_headers = {**default_headers, **custom_headers}\n            else:\n                merged_headers = default_headers\n\n            response = self.client.messages.create(\n                **params,\n                extra_headers=merged_headers\n            )\n            tool_calls = [\n                block for block in response.content if block.type == \"tool_use\"\n            ]\n\n            if self.verbose:\n                for block in response.content:\n                    if block.type == \"text\":\n                        print(f\"\\n[{self.name}] Output: {block.text}\")\n                    elif block.type == \"tool_use\":\n                        params_str = \", \".join(\n                            [f\"{k}={v}\" for k, v in block.input.items()]\n                        )\n                        print(\n                            f\"\\n[{self.name}] Tool call: \"\n                            f\"{block.name}({params_str})\"\n                        )\n\n            await self.history.add_message(\n                \"assistant\", response.content, response.usage\n            )\n\n            if tool_calls:\n                tool_results = await execute_tools(\n                    tool_calls,\n                    tool_dict,\n                )\n                if self.verbose:\n                    for block in tool_results:\n                        print(\n                            f\"\\n[{self.name}] Tool result: \"\n                            f\"{block.get('content')}\"\n                        )\n                await self.history.add_message(\"user\", tool_results)\n            else:\n                return response\n\n    async def run_async(self, user_input: str) -> list[dict[str, Any]]:\n        \"\"\"Run agent with MCP tools asynchronously.\"\"\"\n        async with AsyncExitStack() as stack:\n            original_tools = list(self.tools)\n\n            try:\n                mcp_tools = await setup_mcp_connections(\n                    self.mcp_servers, stack\n                )\n                self.tools.extend(mcp_tools)\n                return await self._agent_loop(user_input)\n            finally:\n                self.tools = original_tools\n\n    def run(self, user_input: str) -> list[dict[str, Any]]:\n        \"\"\"Run agent synchronously\"\"\"\n        return asyncio.run(self.run_async(user_input))\n"
  },
  {
    "path": "agents/agent_demo.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Agent Demo with Simplified Connection Management\\n\",\n    \"\\n\",\n    \"This notebook demonstrates an agent using multiple tool types with simplified connection management:\\n\",\n    \"1. **Local Calculator**: Mathematical expressions via MCP\\n\",\n    \"2. **Brave Search**: Web search via MCP (if API key available)\\n\",\n    \"3. **Think Tool**: Internal reasoning tool\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 1,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import os\\n\",\n    \"import sys\\n\",\n    \"import nest_asyncio\\n\",\n    \"nest_asyncio.apply()\\n\",\n    \"\\n\",\n    \"parent_dir = os.path.dirname(os.getcwd())\\n\",\n    \"sys.path.insert(0, parent_dir)\\n\",\n    \"from agents.agent import Agent, ModelConfig\\n\",\n    \"from agents.tools.think import ThinkTool\\n\",\n    \"from agents.tools.web_search import WebSearchServerTool\\n\",\n    \"from agents.tools.code_execution import CodeExecutionServerTool\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# 1. Configure tools and MCP servers\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 2,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Calculator server configured: Yes\\n\",\n      \"Brave API key available: Yes\\n\",\n      \"Brave search server configured: Yes\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"# Standard Python tool\\n\",\n    \"think_tool = ThinkTool()\\n\",\n    \"\\n\",\n    \"# Python MCP server\\n\",\n    \"calculator_server_path = os.path.abspath(os.path.join(os.getcwd(), \\\"tools/calculator_mcp.py\\\"))\\n\",\n    \"calculator_server = {\\n\",\n    \"    \\\"type\\\": \\\"stdio\\\",\\n\",\n    \"    \\\"command\\\": \\\"python\\\",\\n\",\n    \"    \\\"args\\\": [calculator_server_path]\\n\",\n    \"}\\n\",\n    \"print(f\\\"Calculator server configured: {'Yes' if calculator_server else 'No'}\\\")\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"# Brave MCP server written in TypeScript\\n\",\n    \"\\n\",\n    \"brave_api_key = os.environ.get(\\\"BRAVE_API_KEY_BASE_DATA\\\", \\\"\\\")\\n\",\n    \"print(f\\\"Brave API key available: {'Yes' if brave_api_key else 'No'}\\\")\\n\",\n    \"brave_search_server = {\\n\",\n    \"    \\\"type\\\": \\\"stdio\\\",\\n\",\n    \"    \\\"command\\\": \\\"npx\\\",\\n\",\n    \"    \\\"args\\\": [\\\"-y\\\", \\\"@modelcontextprotocol/server-brave-search\\\"],\\n\",\n    \"    \\\"env\\\": {\\n\",\n    \"        \\\"BRAVE_API_KEY\\\": brave_api_key, \\n\",\n    \"        \\\"PATH\\\": f\\\"{os.path.dirname('npx')}:\\\" + os.environ.get(\\\"PATH\\\", \\\"\\\")\\n\",\n    \"    }\\n\",\n    \"}\\n\",\n    \"print(f\\\"Brave search server configured: {'Yes' if brave_search_server else 'No'}\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# 2. Initialize agent with combined tools\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 3,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"\\n\",\n      \"[Multi-Tool Agent] Agent initialized\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"# Create agent config \\n\",\n    \"system_prompt = \\\"\\\"\\\"\\n\",\n    \"You are a helpful assistant with access to:\\n\",\n    \"1. Web search (brave_web_search, brave_local_search)\\n\",\n    \"2. Mathematical calculator (calculate) \\n\",\n    \"3. A tool to think and reason (think)\\n\",\n    \"\\n\",\n    \"Always use the most appropriate tool for each task.\\n\",\n    \"\\\"\\\"\\\"\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"# Initialize agent with standard tools and MCP servers\\n\",\n    \"agent = Agent(\\n\",\n    \"    name=\\\"Multi-Tool Agent\\\",\\n\",\n    \"    system=system_prompt,\\n\",\n    \"    tools=[think_tool], \\n\",\n    \"    mcp_servers=[brave_search_server, calculator_server], \\n\",\n    \"    config=ModelConfig(\\n\",\n    \"        model=\\\"claude-3-7-sonnet-20250219\\\", \\n\",\n    \"        max_tokens=4096,\\n\",\n    \"        temperature=1.0\\n\",\n    \"    ),\\n\",\n    \"    verbose=True\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# 3. Run queries\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 4,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Loaded 3 MCP tools from 2 servers.\\n\",\n      \"\\n\",\n      \"[Multi-Tool Agent] Received: What's the square root of the OKC population in 2022\\n\",\n      \"\\n\",\n      \"[Multi-Tool Agent] Output: I'll help you find the square root of Oklahoma City's population in 2022. First, I need to look up the population data, then I can calculate the square root.\\n\",\n      \"\\n\",\n      \"[Multi-Tool Agent] Tool call: brave_web_search(query=Oklahoma City population 2022 census data)\\n\",\n      \"\\n\",\n      \"[Multi-Tool Agent] Tool result: Title: Oklahoma Cities by Population (2025)\\n\",\n      \"Description: The most populous cities in Oklahoma are <strong>Oklahoma City with 702,767</strong>, Tulsa at 411,894, Norman with 130,046, Broken Arrow at 119,194, and Edmond with 98,103. You can get the most current Census population data for all 834 Oklahoma cities, sorted from the most populous cities in Oklahoma to the ...\\n\",\n      \"URL: https://www.oklahoma-demographics.com/cities_by_population\\n\",\n      \"\\n\",\n      \"Title: U.S. Census Bureau QuickFacts: Oklahoma City city, Oklahoma\\n\",\n      \"Description: QuickFacts provides statistics for all states and counties. Also for cities and towns with a population of <strong>5,000 or more</strong>. ... Methodology differences may exist between data sources, and so estimates from different sources are not comparable.\\n\",\n      \"URL: https://www.census.gov/quickfacts/fact/table/oklahomacitycityoklahoma/PST045224\\n\",\n      \"\\n\",\n      \"Title: Oklahoma City lands in top 20 of largest US cities by population: New census data\\n\",\n      \"Description: More: 10 Oklahoma cities where population is growing the fastest · Oklahoma City grew by about 1% between 2022 and 2023, adding <strong>7,589 people</strong>. And between the 2010 and 2020 Censuses, Oklahoma City was one of 14 American cities that added more than 100,000 people.\\n\",\n      \"URL: https://www.oklahoman.com/story/news/2024/05/17/oklahoma-city-population-top-20-us-census-data-growth/73729737007/\\n\",\n      \"\\n\",\n      \"Title: Oklahoma City - Wikipedia\\n\",\n      \"Description: Oklahoma City (/ˌoʊkləˈhoʊmə ... it ranks 20th in population among U.S. cities and 8th in the Southern United States, with a population of <strong>681,054 at the 2020 census</strong>....\\n\",\n      \"URL: https://en.wikipedia.org/wiki/Oklahoma_City\\n\",\n      \"\\n\",\n      \"Title: Oklahoma City city, Oklahoma - Census Bureau Profile\\n\",\n      \"Description: We&#x27;re sorry but this website doesn&#x27;t work properly without JavaScript enabled. Please enable it to continue\\n\",\n      \"URL: https://data.census.gov/profile/Oklahoma_City_city,_Oklahoma?g=160XX00US4055000\\n\",\n      \"\\n\",\n      \"Title: Oklahoma City, Oklahoma Population 2025\\n\",\n      \"Description: Oklahoma City is a city located in Cleveland County, Oklahoma. Oklahoma City has a <strong>2025 population of 715,893</strong>. Oklahoma City is currently growing at a rate of 0.93% annually and its population has increased by 4.8% since the most recent census, which recorded a population of 683,078 in 2020.\\n\",\n      \"URL: https://worldpopulationreview.com/us-cities/oklahoma/oklahoma-city\\n\",\n      \"\\n\",\n      \"Title: Oklahoma Demographics | Current Census Data\\n\",\n      \"Description: Get the Oklahoma demographic data that you need. You can get the most current Oklahoma data from the US Census Bureau without spending 40 high-value hours searching the internet. The Oklahoma population is <strong>4,053,824 people</strong> according to the US Census Bureau&#x27;s 2023 Population Estimates Program.\\n\",\n      \"URL: https://www.oklahoma-demographics.com/\\n\",\n      \"\\n\",\n      \"Title: Oklahoma City, OK - Profile data - Census Reporter\\n\",\n      \"Description: a little less than the figure in the Oklahoma City, OK Metro Area: <strong>23.5 14,717,310</strong> (±0.2 / ±145,724)\\n\",\n      \"URL: https://censusreporter.org/profiles/16000US4055000-oklahoma-city-ok/\\n\",\n      \"\\n\",\n      \"Title: Oklahoma population by year, county, race, & more | USAFacts\\n\",\n      \"Description: The ages, races, and <strong>population</strong> density of <strong>Oklahoma</strong> tell a story. Understand the shifts in demographic trends with these charts visualizing decades of <strong>population</strong> <strong>data</strong>. ... Florida K-12 public teachers are paid the lowest average salary when adjusted for cost of living.Published on November 8, <strong>2022</strong> ... <strong>The</strong> <strong>Census</strong> ...\\n\",\n      \"URL: https://usafacts.org/data/topics/people-society/population-and-demographics/our-changing-population/state/oklahoma/\\n\",\n      \"\\n\",\n      \"Title: Resident Population in Oklahoma City, OK (MSA) (OKCPOP) | FRED | St. Louis Fed\\n\",\n      \"Description: Graph and download economic <strong>data</strong> for Resident <strong>Population</strong> <strong>in</strong> <strong>Oklahoma</strong> <strong>City</strong>, OK (MSA) (OKCPOP) from 2000 to 2024 about <strong>Oklahoma</strong> <strong>City</strong>, OK, residents, <strong>population</strong>, and USA.\\n\",\n      \"URL: https://fred.stlouisfed.org/series/OKCPOP\\n\",\n      \"\\n\",\n      \"[Multi-Tool Agent] Output: Based on the search results, I can see multiple population figures for Oklahoma City in different years. The most relevant information for 2022 specifically isn't directly provided, but I can see:\\n\",\n      \"\\n\",\n      \"- 2020 census: 681,054 people\\n\",\n      \"- 2023 estimate showing growth of 7,589 people from 2022\\n\",\n      \"- 2025 projection: 715,893 people\\n\",\n      \"\\n\",\n      \"Let me search more specifically for the 2022 population:\\n\",\n      \"\\n\",\n      \"[Multi-Tool Agent] Tool call: brave_web_search(query=Oklahoma City population 2022 exact figure)\\n\",\n      \"\\n\",\n      \"[Multi-Tool Agent] Tool result: Title: Oklahoma City, Oklahoma Population 2025\\n\",\n      \"Description: Oklahoma City is a city located in Cleveland County, Oklahoma. Oklahoma City has a <strong>2025 population of 715,893</strong>.\\n\",\n      \"URL: https://worldpopulationreview.com/us-cities/oklahoma/oklahoma-city\\n\",\n      \"\\n\",\n      \"Title: Oklahoma City Metro Area Population 1950-2025\\n\",\n      \"Description: The metro area population of Oklahoma City in 2022 was <strong>1,008,000</strong>, a 1% increase from 2021.\\n\",\n      \"URL: https://www.macrotrends.net/cities/23088/oklahoma-city/population\\n\",\n      \"\\n\",\n      \"Title: Oklahoma City city, Oklahoma - Census Bureau Profile\\n\",\n      \"Description: We&#x27;re sorry but this website doesn&#x27;t work properly without JavaScript enabled. Please enable it to continue\\n\",\n      \"URL: https://data.census.gov/profile/Oklahoma_City_city,_Oklahoma?g=160XX00US4055000\\n\",\n      \"\\n\",\n      \"Title: Oklahoma City - Wikipedia\\n\",\n      \"Description: The Oklahoma City metropolitan area has an estimated <strong>1.49 million</strong> residents, the largest metropolitan area in the state and 42nd-most populous in the nation. The city limits extend into Canadian, Cleveland, and Pottawatomie counties; however, areas beyond Oklahoma County primarily consist of ...\\n\",\n      \"URL: https://en.wikipedia.org/wiki/Oklahoma_City\\n\",\n      \"\\n\",\n      \"Title: U.S. Census Bureau QuickFacts: Oklahoma City city, Oklahoma\\n\",\n      \"Description: QuickFacts provides statistics for all states and counties. Also for cities and towns with a population of <strong>5,000 or more</strong>.\\n\",\n      \"URL: https://www.census.gov/quickfacts/oklahomacitycityoklahoma\\n\",\n      \"\\n\",\n      \"Title: Oklahoma City, OK | Data USA\\n\",\n      \"Description: As of 2023, 12% of Oklahoma City, OK residents <strong>(82.6k people</strong>) were born outside of the United States, which is lower than the national average of 13.8%. In 2022, the percentage of foreign-born citizens in Oklahoma City, OK was 11.8%, meaning that the rate has been increasing.\\n\",\n      \"URL: https://datausa.io/profile/geo/oklahoma-city-ok\\n\",\n      \"\\n\",\n      \"Title: Oklahoma City lands in top 20 of largest US cities by population: New census data\\n\",\n      \"Description: More: 10 Oklahoma cities where population is growing the fastest · Oklahoma City grew by about 1% between 2022 and 2023, adding <strong>7,589 people</strong>.\\n\",\n      \"URL: https://www.oklahoman.com/story/news/2024/05/17/oklahoma-city-population-top-20-us-census-data-growth/73729737007/\\n\",\n      \"\\n\",\n      \"Title: Oklahoma Cities by Population (2025)\\n\",\n      \"Description: The most populous cities in Oklahoma are Oklahoma City with <strong>702,767</strong>, Tulsa at 411,894, Norman with 130,046, Broken Arrow at 119,194, and Edmond with 98,103.\\n\",\n      \"URL: https://www.oklahoma-demographics.com/cities_by_population\\n\",\n      \"\\n\",\n      \"Title: Oklahoma City, OK\\n\",\n      \"Description: Oklahoma City, Oklahoma has a population of <strong>688,700</strong>. The median household income is $66,700 and 15% of residents live below the federal poverty level. The racial and ethnic composition is 22% Hispanic, 51% White, 13% Black, 4% Asian and 2% American Indian or Alaska Native.\\n\",\n      \"URL: https://bigcitieshealthdata.org/city/oklahoma-city-ok/\\n\",\n      \"\\n\",\n      \"Title: Oklahoma Cities by Population 2025\\n\",\n      \"Description: Oklahoma City is the biggest city in Oklahoma, with a population of <strong>715,893 in 2025</strong>.\\n\",\n      \"URL: https://worldpopulationreview.com/us-cities/oklahoma\\n\"\n     ]\n    },\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"\\n\",\n      \"[Multi-Tool Agent] Output: I still don't see a specific figure for exactly 2022, but I can gather enough information to make a reasonable estimate.\\n\",\n      \"\\n\",\n      \"[Multi-Tool Agent] Tool call: think(thought=From the search results, I can see:\\n\",\n      \"1. The 2020 census population was 681,054\\n\",\n      \"2. Oklahoma City grew by about 1% between 2022 and 2023, adding 7,589 people\\n\",\n      \"3. This implies the 2022 population was approximately 7,589/0.01 = 758,900 people\\n\",\n      \"4. But this seems too high compared to other figures like 702,767 and 688,700 mentioned\\n\",\n      \"\\n\",\n      \"Let me reconsider. If Oklahoma City added 7,589 people between 2022-2023 and that represented a 1% growth, then the 2022 population would be:\\n\",\n      \"7,589 ÷ 0.01 = 758,900\\n\",\n      \"\\n\",\n      \"However, this contradicts other data points. Perhaps the clearest estimate is from the second search result which states: \\\"The metro area population of Oklahoma City in 2022 was 1,008,000\\\". But this refers to the metro area (larger than just the city proper).\\n\",\n      \"\\n\",\n      \"Another data point from the first search states \\\"Oklahoma City has a 2025 population of 715,893\\\" and mentions it \\\"has increased by 4.8% since the most recent census, which recorded a population of 683,078 in 2020.\\\"\\n\",\n      \"\\n\",\n      \"If the growth was relatively steady from 2020 to 2025, the annual growth rate would be about 0.96% per year. \\n\",\n      \"Using that rate, the 2022 population would be approximately:\\n\",\n      \"683,078 × (1.0096)² = 683,078 × 1.0193 = 696,272\\n\",\n      \"\\n\",\n      \"This seems most consistent with the other data points. I'll use 696,272 as the best estimate for Oklahoma City's 2022 population.)\\n\",\n      \"\\n\",\n      \"[Multi-Tool Agent] Tool result: Thinking complete!\\n\",\n      \"\\n\",\n      \"[Multi-Tool Agent] Output: Based on the available data, I can estimate that Oklahoma City's population in 2022 was approximately 696,272 people. Now I'll calculate the square root of this number.\\n\",\n      \"\\n\",\n      \"[Multi-Tool Agent] Tool call: calculator(number1=696272, number2=0, operator=sqrt)\\n\",\n      \"\\n\",\n      \"[Multi-Tool Agent] Tool result: Result: 834.4291461831855\\n\",\n      \"\\n\",\n      \"[Multi-Tool Agent] Output: The square root of Oklahoma City's population in 2022 (estimated at 696,272 people) is approximately 834.43.\\n\"\n     ]\n    },\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"Message(id='msg_016wLY8JCd5baUgaWEHcrage', content=[TextBlock(text=\\\"The square root of Oklahoma City's population in 2022 (estimated at 696,272 people) is approximately 834.43.\\\", type='text')], model='claude-3-7-sonnet-20250219', role='assistant', stop_reason='end_turn', stop_sequence=None, type='message', usage=Usage(input_tokens=5, output_tokens=34, cache_creation_input_tokens=150, cache_read_input_tokens=3881, service_tier='standard'))\"\n      ]\n     },\n     \"execution_count\": 4,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"# Example query\\n\",\n    \"agent.run(\\\"What's the square root of the OKC population in 2022\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 5,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Loaded 3 MCP tools from 2 servers.\\n\",\n      \"\\n\",\n      \"[Multi-Tool Agent] Received: How many bananas will fit in an Toyota GR86?\\n\",\n      \"\\n\",\n      \"[Multi-Tool Agent] Output: That's an interesting question! To answer how many bananas will fit in a Toyota GR86, I'll need to find information about the car's cargo capacity and then estimate how many bananas could fit in that space.\\n\",\n      \"\\n\",\n      \"[Multi-Tool Agent] Tool call: brave_web_search(query=Toyota GR86 cargo capacity trunk volume)\\n\",\n      \"\\n\",\n      \"[Multi-Tool Agent] Tool result: Title: Cargo space. What can it fit? : r/GR86\\n\",\n      \"Description: 11 votes, 36 comments. 42K subscribers in the <strong>GR86</strong> community. For anyone interested in the <strong>Toyota</strong> <strong>GR86</strong> &amp; Subaru BRZ (&#x27;22+).\\n\",\n      \"URL: https://www.reddit.com/r/GR86/comments/16xh6ii/cargo_space_what_can_it_fit/\\n\",\n      \"\\n\",\n      \"Title: 2024 Toyota GR86 Review, Pricing, and Specs\\n\",\n      \"Description: The 2024 <strong>Toyota</strong> <strong>GR86</strong> turns every day into an autocross day thanks to its playful chassis and easy-to-slide rear-wheel drive antics.\\n\",\n      \"URL: https://www.caranddriver.com/toyota/gr-86/specs\\n\",\n      \"\\n\",\n      \"Title: 2023 Toyota GR86 Interior, Cargo Space & Seating | U.S. News\\n\",\n      \"Description: &quot;You might not think practicality was a consideration with the GR86, but it is surprisingly useful for weekly errands. Its relatively small <strong>6.3 cubic feet</strong> of trunk space is hampered by its short height. But when the seats are folded, Toyota says the GR86 can hold a set of its own replacement ...\\n\",\n      \"URL: https://cars.usnews.com/cars-trucks/toyota/gr86/2023/interior\\n\",\n      \"\\n\",\n      \"Title: 2025 Toyota GR86 Review, Pricing, and Specs\\n\",\n      \"Description: DIMENSIONS Wheelbase: 101.4 in Length: 167.9 in Width: 69.9 in Height: 51.6 in Passenger Volume: <strong>78 ft3 Trunk Volume: 6 ft3</strong> Curb Weight: 2838 lb · C/D TEST RESULTS 60 mph: 5.4 sec 100 mph: 13.8 sec 1/4-Mile: 14.0 sec @ 101 mph 130 mph: 27.2 sec Results above omit 1-ft rollout of 0.3 sec.\\n\",\n      \"URL: https://www.caranddriver.com/toyota/gr-86\\n\",\n      \"\\n\",\n      \"Title: Toyota GR86 dimensions, boot space and similars\\n\",\n      \"Description: <strong>Toyota</strong> <strong>GR86</strong> dimensions with interior photos, boot space and measurements of length, width and height. Previous cars and similars in size.\\n\",\n      \"URL: https://www.automobiledimension.com/model/toyota/gr86\\n\",\n      \"\\n\",\n      \"Title: Trunk Size | Toyota GR86 Forum - GT86 Forum, Subaru BRZ Forum, Scion ...\\n\",\n      \"Description: It still surprises me that I can fit so much in the <strong>trunk</strong>. I am able to fit my entire paintball gear bag, pods, cleats, 2 cases of paint, mask case, small ice chest, and my extra clothes and smaller items. Gotta enjoy the little things with these beautiful cars!\\n\",\n      \"URL: https://www.gr86.org/threads/trunk-size.8574/\\n\",\n      \"\\n\",\n      \"Title: Toyota GR86 Cargo Space | Toyota GR86 Forum - GT86 Forum, Subaru BRZ Forum, Scion ...\\n\",\n      \"Description: Not sure if serious or market differences, but it is mandated in all cars in the U.S. if the <strong>trunk</strong> is much bigger than a shoebox lol. ... This is an older thread, you may not receive a response, and could be reviving an old thread. Please consider creating a new thread. ... <strong>Toyota</strong> <strong>GR86</strong> Forum ...\\n\",\n      \"URL: https://www.gr86.org/threads/toyota-gr86-cargo-space.372/\\n\",\n      \"\\n\",\n      \"Title: GR86 - GR86 Boot Dimensions | Toyota GR Forum for owners of GR Yaris, GR86, GR Supra and GR Corolla\\n\",\n      \"Description: During my time with the GR86, I tried to take accurate measurements of the boot dimensions (in centimetres), as shown below. Sorry the pictures are not exactly to scale - my MS Paint skills are rather limited! = The boot opening is <strong>60cm at its narrowest (at the bottom), 80cm at the widest</strong>...\\n\",\n      \"URL: https://www.gr-zoo.com/threads/gr86-boot-dimensions.2567/\\n\",\n      \"\\n\",\n      \"Title: 2025 Toyota GR86 Interior, Cargo Space & Seating | U.S. News\\n\",\n      \"Description: Few sports cars have large trunks, and the GR86 is no exception. It has just <strong>6.3 cubic feet</strong> of trunk space, which is not much even for this class. You can fit several grocery bags or a couple of carry-on bags with the rear seats upright.\\n\",\n      \"URL: https://cars.usnews.com/cars-trucks/toyota/gr86/interior\\n\",\n      \"\\n\",\n      \"Title: Toyota GR86 (2025) boot space & practicality\\n\",\n      \"Description: Read what our experts think about the <strong>Toyota</strong> <strong>GR86</strong>&#x27;s practicality, boot size and overall dimensions, how it compares to its rivals and more.\\n\",\n      \"URL: https://www.parkers.co.uk/toyota/gr86/review/practicality/\\n\",\n      \"\\n\",\n      \"[Multi-Tool Agent] Output: Now I need to get information about the average size of a banana to make this calculation:\\n\",\n      \"\\n\",\n      \"[Multi-Tool Agent] Tool call: brave_web_search(query=average banana size dimensions volume)\\n\",\n      \"\\n\",\n      \"[Multi-Tool Agent] Tool result: Title: Banana for scale - Universal converter - Unit conversion for all!\\n\",\n      \"Description: Introducing the <strong>banana</strong> as the universal measurement unit, all conversion-related confusion can be avoided. Use this handy app to convert imperial and metric measurements to universal <strong>bananas</strong>.\\n\",\n      \"URL: http://bananaforscale.info/\\n\",\n      \"\\n\",\n      \"Title: How big is a banana? - Quora\\n\",\n      \"Description: Answer (1 of 10): How big is a <strong>banana</strong>? Is that your <strong>banana</strong> or my <strong>banana</strong>? Musa Ingens, the world&#x27;s largest <strong>banana</strong> tree growing in the forest of Papua New Guinea and Indonesia. It is the tallest species of <strong>bananas</strong> and the world&#x27;s largest herbaceous plant. The <strong>size</strong> of the Musa Ingens <strong>banana</strong> is imp...\\n\",\n      \"URL: https://www.quora.com/How-big-is-a-banana\\n\",\n      \"\\n\",\n      \"Title: How Many Calories and Carbs Are in a Banana?\\n\",\n      \"Description: <strong>Bananas</strong> are extremely healthy and nutritious. Most people know this, but many wonder how many calories and carbs <strong>bananas</strong> contain.\\n\",\n      \"URL: https://www.healthline.com/nutrition/bananas-calories-carbs\\n\",\n      \"\\n\",\n      \"Title: Banana - Wikipedia\\n\",\n      \"Description: As of 2018, <strong>bananas</strong> are exported in larger <strong>volume</strong> and to a larger value than any other fruit. In 2022, world production of <strong>bananas</strong> and plantains combined was 179 million tonnes, led by India and China with a combined total of 26% of global production. Other major producers were Uganda, Indonesia, ...\\n\",\n      \"URL: https://en.wikipedia.org/wiki/Banana\\n\",\n      \"\\n\",\n      \"Title: On the Usefulness of Bananas: A New System of Measurement | by Liam Foley | Medium\\n\",\n      \"Description: According to a study conducted by Mahmoud Soltani of the University of Tehran, on average, a fully ripe banana fruit of the Cavendish variety measures 200.25 millimetres in length, with a mass of 180.56 grams, and an ellipsoid volume of <strong>156.10 cubic centimetres</strong>.\\n\",\n      \"URL: https://medium.com/@liamfoley2430/on-the-usefulness-of-bananas-a-new-system-of-measurement-76c50d8bb89a\\n\",\n      \"\\n\",\n      \"Title: Banana-based Length & Distance Unit Converter | Convert To Bananas\\n\",\n      \"Description: Convert any length to <strong>bananas</strong> (and to useful units as well)\\n\",\n      \"URL: https://www.converttobananas.com/\\n\",\n      \"\\n\",\n      \"Title: r/atlanticdiscussions on Reddit: HOW LONG IS THE, UM, AVERAGE BANANA? And why do you all want to know so bad?\\n\",\n      \"Description: As a writer on the internet, I often feel as though I’m getting a peek into the American mind. Recently, while looking at search data, I discovered…\\n\",\n      \"URL: https://www.reddit.com/r/atlanticdiscussions/comments/pbenqf/how_long_is_the_um_average_banana_and_why_do_you/\\n\",\n      \"\\n\",\n      \"Title: How Long Is the, Um, Average Banana?\\n\",\n      \"Description: <strong>Bananas</strong> come in a total of five <strong>sizes</strong> — extra small, small, medium, large and extra large. But how long is the <strong>average</strong> <strong>banana</strong>?\\n\",\n      \"URL: https://melmagazine.com/en-us/story/how-long-is-a-banana\\n\",\n      \"\\n\",\n      \"Title: r/theydidthemath on Reddit: [Request] Assuming the bananas are normal size how many rooms would 102,769 bananas fill?\\n\",\n      \"Description: The average volume of a banana is <strong>156.1 cm³</strong>. Assuming you mean whole, uncrushed bananas, google search results peg the packing effeciency around 65%.\\n\",\n      \"URL: https://www.reddit.com/r/theydidthemath/comments/rdiyis/request_assuming_the_bananas_are_normal_size_how/\\n\",\n      \"\\n\",\n      \"Title: How big is your banana?\\n\",\n      \"Description: BMI Calculator Enter your details: Height (cm): Weight (kg): BMI (kg/m2) is Category is\\n\",\n      \"URL: https://healthieryou.net.au/wp-content/uploads/2015/11/How-big-is-your-banana-Article.pdf\\n\"\n     ]\n    },\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"\\n\",\n      \"[Multi-Tool Agent] Output: Now I have the information I need to calculate how many bananas will fit in a Toyota GR86. Let me think through this:\\n\",\n      \"\\n\",\n      \"[Multi-Tool Agent] Tool call: think(thought=From the search results, I found:\\n\",\n      \"\\n\",\n      \"1. Toyota GR86 trunk capacity:\\n\",\n      \"   - 6.3 cubic feet of trunk space (according to multiple sources)\\n\",\n      \"   - This equals approximately 178,385 cubic centimeters (6.3 ft³ × 28,316.85 cm³/ft³)\\n\",\n      \"\\n\",\n      \"2. Average banana volume:\\n\",\n      \"   - A fully ripe Cavendish banana has a volume of approximately 156.10 cubic centimeters\\n\",\n      \"\\n\",\n      \"3. Packing efficiency:\\n\",\n      \"   - When packing irregular objects like bananas, we need to account for the space between them\\n\",\n      \"   - A Reddit comment mentioned packing efficiency of around 65% for bananas\\n\",\n      \"   - This means only about 65% of the total volume can be filled with bananas due to gaps\\n\",\n      \"\\n\",\n      \"So to calculate how many bananas will fit in the Toyota GR86 trunk:\\n\",\n      \"- Available volume with packing efficiency: 178,385 cm³ × 0.65 = 115,950 cm³\\n\",\n      \"- Number of bananas: 115,950 cm³ ÷ 156.10 cm³ = 743 bananas\\n\",\n      \"\\n\",\n      \"However, the Toyota GR86 also has an interior cabin with a passenger volume of approximately 78 ft³ (according to one of the search results). If we were to fill the entire car with bananas (which is not practical but answers the hypothetical question):\\n\",\n      \"\\n\",\n      \"Total car volume = 78 ft³ (passenger) + 6.3 ft³ (trunk) = 84.3 ft³ = 2,387,110 cm³\\n\",\n      \"\\n\",\n      \"With 65% packing efficiency:\\n\",\n      \"- Available volume: 2,387,110 cm³ × 0.65 = 1,551,622 cm³\\n\",\n      \"- Number of bananas: 1,551,622 cm³ ÷ 156.10 cm³ = 9,940 bananas\\n\",\n      \"\\n\",\n      \"So, approximately 9,940 bananas could theoretically fit in an entire Toyota GR86 if completely filled, or about 743 bananas in just the trunk.)\\n\",\n      \"\\n\",\n      \"[Multi-Tool Agent] Tool result: Thinking complete!\\n\",\n      \"\\n\",\n      \"[Multi-Tool Agent] Output: # How Many Bananas Will Fit in a Toyota GR86\\n\",\n      \"\\n\",\n      \"Based on my research, I can provide you with an estimate of how many bananas would fit in a Toyota GR86.\\n\",\n      \"\\n\",\n      \"## The Data:\\n\",\n      \"\\n\",\n      \"- **Toyota GR86 trunk capacity**: 6.3 cubic feet (approximately 178,385 cubic centimeters)\\n\",\n      \"- **Toyota GR86 passenger volume**: 78 cubic feet\\n\",\n      \"- **Average banana volume**: 156.10 cubic centimeters per banana\\n\",\n      \"- **Packing efficiency**: Approximately 65% (accounting for inevitable gaps between irregularly shaped bananas)\\n\",\n      \"\\n\",\n      \"## The Calculation:\\n\",\n      \"\\n\",\n      \"If we're talking about filling just the trunk:\\n\",\n      \"- Available volume with packing efficiency: 178,385 cm³ × 0.65 = 115,950 cm³\\n\",\n      \"- Number of bananas in trunk: 115,950 cm³ ÷ 156.10 cm³ = approximately **743 bananas**\\n\",\n      \"\\n\",\n      \"If we're considering the entire car (including passenger compartment):\\n\",\n      \"- Total car volume: 84.3 cubic feet (2,387,110 cm³)\\n\",\n      \"- Available volume with packing efficiency: 2,387,110 cm³ × 0.65 = 1,551,622 cm³\\n\",\n      \"- Total number of bananas: 1,551,622 cm³ ÷ 156.10 cm³ = approximately **9,940 bananas**\\n\",\n      \"\\n\",\n      \"So, you could fit approximately 743 bananas in the trunk alone, or nearly 10,000 bananas if you filled the entire Toyota GR86 (though that would leave no room for a driver!).\\n\"\n     ]\n    },\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"Message(id='msg_01NC46TLNZSG97ZH5v7b7YcN', content=[TextBlock(text=\\\"# How Many Bananas Will Fit in a Toyota GR86\\\\n\\\\nBased on my research, I can provide you with an estimate of how many bananas would fit in a Toyota GR86.\\\\n\\\\n## The Data:\\\\n\\\\n- **Toyota GR86 trunk capacity**: 6.3 cubic feet (approximately 178,385 cubic centimeters)\\\\n- **Toyota GR86 passenger volume**: 78 cubic feet\\\\n- **Average banana volume**: 156.10 cubic centimeters per banana\\\\n- **Packing efficiency**: Approximately 65% (accounting for inevitable gaps between irregularly shaped bananas)\\\\n\\\\n## The Calculation:\\\\n\\\\nIf we're talking about filling just the trunk:\\\\n- Available volume with packing efficiency: 178,385 cm³ × 0.65 = 115,950 cm³\\\\n- Number of bananas in trunk: 115,950 cm³ ÷ 156.10 cm³ = approximately **743 bananas**\\\\n\\\\nIf we're considering the entire car (including passenger compartment):\\\\n- Total car volume: 84.3 cubic feet (2,387,110 cm³)\\\\n- Available volume with packing efficiency: 2,387,110 cm³ × 0.65 = 1,551,622 cm³\\\\n- Total number of bananas: 1,551,622 cm³ ÷ 156.10 cm³ = approximately **9,940 bananas**\\\\n\\\\nSo, you could fit approximately 743 bananas in the trunk alone, or nearly 10,000 bananas if you filled the entire Toyota GR86 (though that would leave no room for a driver!).\\\", type='text')], model='claude-3-7-sonnet-20250219', role='assistant', stop_reason='end_turn', stop_sequence=None, type='message', usage=Usage(input_tokens=4, output_tokens=385, cache_creation_input_tokens=574, cache_read_input_tokens=6630, service_tier='standard'))\"\n      ]\n     },\n     \"execution_count\": 5,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"await agent.run_async(\\\"How many bananas will fit in an Toyota GR86?\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# 4. Example with Anthropic Server Tools\\n\",\n    \"\\n\",\n    \"This example demonstrates using Anthropic's native server tools for web search and code execution.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 9,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"\\n\",\n      \"[Server Tools Agent] Agent initialized\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"# Create Anthropic server tools\\n\",\n    \"web_search_tool = WebSearchServerTool(\\n\",\n    \"    name=\\\"web_search\\\",\\n\",\n    \"    max_uses=5,  # Limit to 5 searches per request\\n\",\n    \"    blocked_domains=[\\\"example.com\\\"]  # Example of blocking specific domains\\n\",\n    \")\\n\",\n    \"\\n\",\n    \"code_execution_tool = CodeExecutionServerTool()\\n\",\n    \"\\n\",\n    \"# Initialize agent with server tools\\n\",\n    \"server_agent = Agent(\\n\",\n    \"    name=\\\"Server Tools Agent\\\",\\n\",\n    \"    system=\\\"\\\"\\\"\\n\",\n    \"You are a helpful assistant with access to:\\n\",\n    \"1. Web search for finding current information\\n\",\n    \"2. Code execution for running Python code\\n\",\n    \"3. Think tool for complex reasoning\\n\",\n    \"\\n\",\n    \"Use these tools effectively to answer questions that require current data or calculations.\\n\",\n    \"\\\"\\\"\\\",\\n\",\n    \"    tools=[think_tool, web_search_tool, code_execution_tool],\\n\",\n    \"    config=ModelConfig(\\n\",\n    \"        model=\\\"claude-sonnet-4-20250514\\\",\\n\",\n    \"        max_tokens=4096,\\n\",\n    \"        temperature=0.7\\n\",\n    \"    ),\\n\",\n    \"    verbose=True\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 10,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"\\n\",\n      \"[Server Tools Agent] Received: \\n\",\n      \"Search for the current population of Tokyo, Japan. \\n\",\n      \"Then write and execute Python code to calculate how many people that would be per square kilometer, \\n\",\n      \"given that Tokyo's area is approximately 2,194 square kilometers.\\n\",\n      \"\\n\",\n      \"\\n\",\n      \"[Server Tools Agent] Output: I'll help you find Tokyo's current population and then calculate the population density. Let me start by searching for the current population data.\\n\",\n      \"\\n\",\n      \"[Server Tools Agent] Output: Now let me search for more specific information about Tokyo's current population, as the results show different figures for different definitions of \\\"Tokyo\\\" (city proper vs metropolitan area).\\n\",\n      \"\\n\",\n      \"[Server Tools Agent] Tool call: think(thought=Now I have a clearer picture of Tokyo's population. From the search results, I can see there are different definitions of \\\"Tokyo\\\":\\n\",\n      \"\\n\",\n      \"1. Tokyo Metropolis (Tokyo-to) - the prefecture/administrative area: Around 14 million people as of 2024\\n\",\n      \"2. The 23 Special Wards (central Tokyo): Around 9.2-9.7 million people\\n\",\n      \"3. Greater Tokyo Area (metropolitan area): Around 37 million people\\n\",\n      \"\\n\",\n      \"For the calculation with the given area of 2,194 square kilometers, this seems to refer to the Tokyo Metropolis (Tokyo-to) area, as the search results mention:\\n\",\n      \"- \\\"metropolis population is 13.9 Million as on 2019 and has area of 2,194 Sq km\\\" (from source 17)\\n\",\n      \"- The surface area of the city metropolis comes to approximately 2,187.66 km² (from source 2)\\n\",\n      \"\\n\",\n      \"So I should use approximately 14 million as the current population for Tokyo Metropolis for the calculation with the 2,194 sq km area.\\n\",\n      \"\\n\",\n      \"Let me write Python code to calculate the population density.)\\n\",\n      \"\\n\",\n      \"[Server Tools Agent] Tool result: Thinking complete!\\n\",\n      \"\\n\",\n      \"[Server Tools Agent] Output: Now I'll write and execute Python code to calculate Tokyo's population density based on the information I found.\\n\",\n      \"\\n\",\n      \"[Server Tools Agent] Output: Based on my search and calculations, here are the key findings:\\n\",\n      \"\\n\",\n      \"## Tokyo's Current Population\\n\",\n      \"\\n\",\n      \"\\n\",\n      \"\\n\",\n      \"[Server Tools Agent] Output: Tokyo Metropolis (Tokyo-to) has a population of over 14 million people as of 2023-2024\\n\",\n      \"\\n\",\n      \"[Server Tools Agent] Output: . The search results show different figures depending on the specific definition of \\\"Tokyo\\\":\\n\",\n      \"\\n\",\n      \"- \\n\",\n      \"\\n\",\n      \"[Server Tools Agent] Output: The 23 wards have a population of 9.2 million\\n\",\n      \"\\n\",\n      \"[Server Tools Agent] Output: \\n\",\n      \"- \\n\",\n      \"\\n\",\n      \"[Server Tools Agent] Output: The metropolis has a population that exceeds 13 million\\n\",\n      \"\\n\",\n      \"[Server Tools Agent] Output: \\n\",\n      \"- \\n\",\n      \"\\n\",\n      \"[Server Tools Agent] Output: The Greater Tokyo Area has 41 million residents as of 2024\\n\",\n      \"\\n\",\n      \"[Server Tools Agent] Output: \\n\",\n      \"\\n\",\n      \"## Population Density Calculation\\n\",\n      \"\\n\",\n      \"Using the current population of approximately **14 million people** for Tokyo Metropolis and the given area of **2,194 square kilometers**, the calculation shows:\\n\",\n      \"\\n\",\n      \"**Tokyo's population density is approximately 6,381 people per square kilometer**\\n\",\n      \"\\n\",\n      \"This aligns well with existing data, as \\n\",\n      \"\\n\",\n      \"[Server Tools Agent] Output: one source indicates Tokyo metropolis has a density of 6,349 persons per square kilometer\\n\",\n      \"\\n\",\n      \"[Server Tools Agent] Output: , confirming our calculation is accurate.\\n\",\n      \"\\n\",\n      \"## Context\\n\",\n      \"\\n\",\n      \"This makes Tokyo one of the most densely populated metropolitan areas in the world. \\n\",\n      \"\\n\",\n      \"[Server Tools Agent] Output: The greater Tokyo metropolitan area is estimated to be over 36 million people and is home to 25% of Japan's population, making it the most populous metropolitan area in the world\\n\",\n      \"\\n\",\n      \"[Server Tools Agent] Output: .\\n\"\n     ]\n    },\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"Message(id='msg_018nAgEXuVMSJCTSrSjcZtrj', content=[TextBlock(text=\\\"Now I'll write and execute Python code to calculate Tokyo's population density based on the information I found.\\\", type='text'), TextBlock(text=None, type='server_tool_use', id='srvtoolu_01JAxi9fcajoScWQWEG1bJ2S', name='code_execution', input={'code': '# Tokyo population and area data\\\\n# Based on search results, Tokyo Metropolis (Tokyo-to) has approximately 14 million people\\\\n# and covers 2,194 square kilometers\\\\n\\\\ntokyo_population = 14_000_000  # Current population of Tokyo Metropolis (2024)\\\\ntokyo_area_sq_km = 2194  # Area in square kilometers\\\\n\\\\n# Calculate population density\\\\npopulation_density = tokyo_population / tokyo_area_sq_km\\\\n\\\\nprint(\\\"Tokyo Population Density Calculation\\\")\\\\nprint(\\\"=\\\" * 40)\\\\nprint(f\\\"Tokyo Metropolis Population: {tokyo_population:,} people\\\")\\\\nprint(f\\\"Tokyo Metropolis Area: {tokyo_area_sq_km:,} square kilometers\\\")\\\\nprint(f\\\"Population Density: {population_density:,.2f} people per square kilometer\\\")\\\\n\\\\n# Let\\\\'s also show some context with rounded numbers\\\\nprint(f\\\"\\\\\\\\nRounded: Approximately {population_density:,.0f} people per square kilometer\\\")\\\\n\\\\n# For comparison, let\\\\'s also calculate for different population estimates\\\\nprint(\\\"\\\\\\\\n\\\" + \\\"=\\\"*50)\\\\nprint(\\\"COMPARISON WITH DIFFERENT POPULATION ESTIMATES:\\\")\\\\nprint(\\\"=\\\"*50)\\\\n\\\\n# Different population estimates from search results\\\\nestimates = [\\\\n    (\\\"Tokyo Metropolis (conservative estimate)\\\", 13_500_000),\\\\n    (\\\"Tokyo Metropolis (current estimate)\\\", 14_000_000),\\\\n    (\\\"Tokyo Metropolis (high estimate)\\\", 14_300_000),\\\\n]\\\\n\\\\nfor description, population in estimates:\\\\n    density = population / tokyo_area_sq_km\\\\n    print(f\\\"{description}: {density:,.0f} people/km²\\\")'}), TextBlock(text=None, type='code_execution_tool_result', tool_use_id='srvtoolu_01JAxi9fcajoScWQWEG1bJ2S', content={'type': 'code_execution_result', 'stdout': 'Tokyo Population Density Calculation\\\\n========================================\\\\nTokyo Metropolis Population: 14,000,000 people\\\\nTokyo Metropolis Area: 2,194 square kilometers\\\\nPopulation Density: 6,381.04 people per square kilometer\\\\n\\\\nRounded: Approximately 6,381 people per square kilometer\\\\n\\\\n==================================================\\\\nCOMPARISON WITH DIFFERENT POPULATION ESTIMATES:\\\\n==================================================\\\\nTokyo Metropolis (conservative estimate): 6,153 people/km²\\\\nTokyo Metropolis (current estimate): 6,381 people/km²\\\\nTokyo Metropolis (high estimate): 6,518 people/km²\\\\n', 'stderr': '', 'return_code': 0, 'content': []}), TextBlock(text=\\\"Based on my search and calculations, here are the key findings:\\\\n\\\\n## Tokyo's Current Population\\\\n\\\\n\\\", type='text'), TextBlock(text='Tokyo Metropolis (Tokyo-to) has a population of over 14 million people as of 2023-2024', type='text', citations=[{'type': 'web_search_result_location', 'cited_text': 'With a population of over 14 million in the city proper in 2023, it is one of the most populous urban areas in the world. ', 'url': 'https://en.wikipedia.org/wiki/Tokyo', 'title': 'Tokyo - Wikipedia', 'encrypted_index': 'Eo8BCioIBBgCIiQ3NmU4ZmI4OC1kOGFhLTQwMzMtYjQ1Mi1lMmZlY2M3MGFiZjkSDNkmb67meDKwvfNSWBoMey/C2mf4C76VGRm/IjB/p9OlOe00CdTjisZ+Soyr8DL2IDH8J+LioeVOOHK7L/3HfUnClNRaZvtHREGHnFQqE0Pko8rhETBI/USi53r6+Vq2i1sYBA=='}, {'type': 'web_search_result_location', 'cited_text': 'The area within the dashed white line is home to 14 million people, as of 2020. ', 'url': 'http://www.statsmapsnpix.com/2022/09/how-big-is-tokyo.html', 'title': 'Stats, Maps n Pix: How big is Tokyo?', 'encrypted_index': 'EpABCioIBBgCIiQ3NmU4ZmI4OC1kOGFhLTQwMzMtYjQ1Mi1lMmZlY2M3MGFiZjkSDN3Dl6f/w5+R2DpZzRoMZiaU1oKXtaWciBkzIjCeqzTw6VNVH9T8pJcQGylS/KdoN8tamtX70LsJ7ygZeDUBnbVxfCebu8Xfpv9tdJAqFN2KZA2z6Q/22MX5gewO0+OfwUtrGAQ='}, {'type': 'web_search_result_location', 'cited_text': 'The area within the dashed white line is home to 14 million people, as of 2020. ', 'url': 'http://www.statsmapsnpix.com/2022/09/how-big-is-tokyo.html', 'title': 'Stats, Maps n Pix: How big is Tokyo?', 'encrypted_index': 'EpABCioIBBgCIiQ3NmU4ZmI4OC1kOGFhLTQwMzMtYjQ1Mi1lMmZlY2M3MGFiZjkSDPgOVKuta9qNyug8dRoMzPFdKW3fR2mvZIK7IjCUZybt1VVAMoVOA3VEOCprNID+ZQWavx0Z9ug53tjoeanpJvyGUIqn65McFNAKksEqFDn6oY/apm+AjLJk908w1NyD4w0OGAQ='}]), TextBlock(text='. The search results show different figures depending on the specific definition of \\\"Tokyo\\\":\\\\n\\\\n- ', type='text'), TextBlock(text='The 23 wards have a population of 9.2 million', type='text', citations=[{'type': 'web_search_result_location', 'cited_text': 'The 23 wards claim a population of 9.2 million, but the metropolis has a population that exceeds 13 million. ', 'url': 'https://worldpopulationreview.com/cities/japan/tokyo', 'title': 'Tokyo Population 2025', 'encrypted_index': 'Eo8BCioIBBgCIiQ3NmU4ZmI4OC1kOGFhLTQwMzMtYjQ1Mi1lMmZlY2M3MGFiZjkSDGHmgL6AaaF2lV1RIBoMst2h5K/VIu2RGDTwIjBLqvkNQPtkPP1V49Wf9IxwZBOyM+HgBvi7rd6yR/Wkjr9LpT3HnIQOT5FetsLzqUoqE5k8jG1Xsz7hyicRcL/9UCJHKU8YBA=='}]), TextBlock(text='\\\\n- ', type='text'), TextBlock(text='The metropolis has a population that exceeds 13 million', type='text', citations=[{'type': 'web_search_result_location', 'cited_text': 'The 23 wards claim a population of 9.2 million, but the metropolis has a population that exceeds 13 million. ', 'url': 'https://worldpopulationreview.com/cities/japan/tokyo', 'title': 'Tokyo Population 2025', 'encrypted_index': 'Eo8BCioIBBgCIiQ3NmU4ZmI4OC1kOGFhLTQwMzMtYjQ1Mi1lMmZlY2M3MGFiZjkSDHsQQzoI+Hz+TiOD7xoMgVi2a9XbrLmJlkNKIjCXGiOzb7fzGY/ueYkCuE280j3RfDhLCJ3YWi6gVboWe2BidSHKRwJBoPbhHAaWwVYqEwZ9kwdNZiabocE1Kbh3m6GW/SMYBA=='}, {'type': 'web_search_result_location', 'cited_text': 'The 23 wards claim a population of 9.2 million, but the metropolis has a population that exceeds 13 million. ', 'url': 'https://worldpopulationreview.com/cities/japan/tokyo', 'title': 'Tokyo Population 2025', 'encrypted_index': 'EpEBCioIBBgCIiQ3NmU4ZmI4OC1kOGFhLTQwMzMtYjQ1Mi1lMmZlY2M3MGFiZjkSDHolkkUxMX/ORAlhZBoMoDhnLJx+SR+8R8aTIjDBKIO2DJG07HcLucjTH1y1LCGBRw1mv/t061+rSVpTbV7iY5DXREoEUGCcMFVasUgqFT9KXif7+m2jD/0yN42wWd02HG+jbRgE'}]), TextBlock(text='\\\\n- ', type='text'), TextBlock(text='The Greater Tokyo Area has 41 million residents as of 2024', type='text', citations=[{'type': 'web_search_result_location', 'cited_text': 'The Greater Tokyo Area, which includes Tokyo and parts of six neighboring prefectures, is the most populous metropolitan area in the world, with 41 mi...', 'url': 'https://en.wikipedia.org/wiki/Tokyo', 'title': 'Tokyo - Wikipedia', 'encrypted_index': 'Eo8BCioIBBgCIiQ3NmU4ZmI4OC1kOGFhLTQwMzMtYjQ1Mi1lMmZlY2M3MGFiZjkSDPu/83J4h+//6WejfhoMabArnZYopPgIgBadIjABzh8N5tuANZ8z7yijY+HpuAP+iw6WDSwqolJU6364du78SvzlqUXbWLujKQdIhMcqE1fHzX0qTHJmNVRZHIt3AcmEBLwYBA=='}]), TextBlock(text=\\\"\\\\n\\\\n## Population Density Calculation\\\\n\\\\nUsing the current population of approximately **14 million people** for Tokyo Metropolis and the given area of **2,194 square kilometers**, the calculation shows:\\\\n\\\\n**Tokyo's population density is approximately 6,381 people per square kilometer**\\\\n\\\\nThis aligns well with existing data, as \\\", type='text'), TextBlock(text='one source indicates Tokyo metropolis has a density of 6,349 persons per square kilometer', type='text', citations=[{'type': 'web_search_result_location', 'cited_text': 'Tokyo population in 2023 is estimated to be 35.8 million, population in 2022 is estimated to be 35.6 million, in 2021 is estimated to be 37.5 million,...', 'url': 'https://www.populationu.com/cities/tokyo-population', 'title': 'Tokyo Population 2025', 'encrypted_index': 'EpEBCioIBBgCIiQ3NmU4ZmI4OC1kOGFhLTQwMzMtYjQ1Mi1lMmZlY2M3MGFiZjkSDH+CHv/68jATiDYFzRoMKun6Si5c+OHOiG9WIjAq6lZhNchC2RxsNLZMgheItJe0yOT53PlDChrsyxJztTkx8aSELTlrH+2V953cZhYqFUMI2gojH59prn81fC8mSgHNmvaKKBgE'}]), TextBlock(text=', confirming our calculation is accurate.\\\\n\\\\n## Context\\\\n\\\\nThis makes Tokyo one of the most densely populated metropolitan areas in the world. ', type='text'), TextBlock(text=\\\"The greater Tokyo metropolitan area is estimated to be over 36 million people and is home to 25% of Japan's population, making it the most populous metropolitan area in the world\\\", type='text', citations=[{'type': 'web_search_result_location', 'cited_text': 'The greater Tokyo metropolitan area, which is spread over 3 prefectures, is much larger and has a population that is estimated to be over 36 million. ...', 'url': 'https://worldpopulationreview.com/cities/japan/tokyo', 'title': 'Tokyo Population 2025', 'encrypted_index': 'EpEBCioIBBgCIiQ3NmU4ZmI4OC1kOGFhLTQwMzMtYjQ1Mi1lMmZlY2M3MGFiZjkSDC3eHB8MpV+6PvSJ9hoMGBBbLOOC80uQmSO4IjCujon0QmWYTtDWe+Po+0YK+bXYbwcEwKmurDHUK1IuU6w2rvi9Rq2XeSecqpnOhNgqFU8JodAjBnotfaYFd1dee+V5IBAlIxgE'}]), TextBlock(text='.', type='text')], model='claude-sonnet-4-20250514', role='assistant', stop_reason='end_turn', stop_sequence=None, type='message', usage=Usage(input_tokens=9, output_tokens=841, cache_creation_input_tokens=928, cache_read_input_tokens=48328, service_tier='standard', server_tool_use={'web_search_requests': 0}), container={'id': 'container_011CQTnr9bhVu3fPTnaT85oA', 'expires_at': '2025-06-24T22:55:24.157374+00:00'})\"\n      ]\n     },\n     \"execution_count\": 10,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"# Example 1: Use web search to find current information and code execution for analysis\\n\",\n    \"server_agent.run(\\\"\\\"\\\"\\n\",\n    \"Search for the current population of Tokyo, Japan. \\n\",\n    \"Then write and execute Python code to calculate how many people that would be per square kilometer, \\n\",\n    \"given that Tokyo's area is approximately 2,194 square kilometers.\\n\",\n    \"\\\"\\\"\\\")\"\n   ]\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \"Python 3 (ipykernel)\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.11.13\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 4\n}\n"
  },
  {
    "path": "agents/test_message_params.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Test suite for Agent message_params functionality.\n\nThis module tests the ability to pass custom parameters to the Claude API\nthrough the Agent's message_params argument, including headers, metadata,\nand API parameters.\n\"\"\"\n\nimport os\nimport sys\n# Add parent directory to path for imports\nsys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\n\nfrom agents.agent import Agent, ModelConfig\n\n\nclass TestMessageParams:\n    \"\"\"Test cases for message_params functionality.\"\"\"\n    \n    def __init__(self, verbose: bool = True):\n        \"\"\"Initialize test suite.\n        \n        Args:\n            verbose: Whether to print detailed output\n        \"\"\"\n        self.verbose = verbose\n        self.passed = 0\n        self.failed = 0\n        \n    def _print(self, message: str) -> None:\n        \"\"\"Print message if verbose mode is on.\"\"\"\n        if self.verbose:\n            print(message)\n            \n    def _run_test(self, test_name: str, test_func: callable) -> None:\n        \"\"\"Run a single test and track results.\n        \n        Args:\n            test_name: Name of the test\n            test_func: Test function to execute\n        \"\"\"\n        self._print(f\"\\n{'='*60}\")\n        self._print(f\"Running: {test_name}\")\n        self._print('='*60)\n        \n        try:\n            test_func()\n            self.passed += 1\n            self._print(f\"✓ {test_name} PASSED\")\n        except Exception as e:\n            self.failed += 1\n            self._print(f\"✗ {test_name} FAILED: {str(e)}\")\n            if self.verbose:\n                import traceback\n                traceback.print_exc()\n    \n    def test_basic_agent(self) -> None:\n        \"\"\"Test agent without message_params to ensure backward compatibility.\"\"\"\n        agent = Agent(\n            name=\"BasicAgent\",\n            system=\"You are a helpful assistant. Be very brief.\",\n            verbose=False\n        )\n        \n        response = agent.run(\"What is 2+2?\")\n        # response is a list of message content blocks\n        assert any(\"4\" in str(block.get(\"text\", \"\")) for block in response if block.get(\"type\") == \"text\")\n        response_text = next((block[\"text\"] for block in response if block.get(\"type\") == \"text\"), \"\")\n        self._print(f\"Response: {response_text}\")\n        \n    def test_custom_headers(self) -> None:\n        \"\"\"Test passing custom headers through message_params.\"\"\"\n        agent = Agent(\n            name=\"HeaderAgent\",\n            system=\"You are a helpful assistant. Be very brief.\",\n            verbose=False,\n            message_params={\n                \"extra_headers\": {\n                    \"X-Custom-Header\": \"test-value\",\n                    \"X-Request-ID\": \"test-12345\"\n                }\n            }\n        )\n        \n        # Verify headers are stored\n        assert \"extra_headers\" in agent.message_params\n        assert agent.message_params[\"extra_headers\"][\"X-Custom-Header\"] == \"test-value\"\n        \n        response = agent.run(\"What is 3+3?\")\n        response_text = next((block[\"text\"] for block in response if block.get(\"type\") == \"text\"), \"\")\n        assert \"6\" in response_text\n        self._print(f\"Response with custom headers: {response_text}\")\n        \n    def test_beta_headers(self) -> None:\n        \"\"\"Test passing beta feature headers.\"\"\"\n        agent = Agent(\n            name=\"BetaAgent\",\n            system=\"You are a helpful assistant. Be very brief.\",\n            verbose=False,\n            message_params={\n                \"extra_headers\": {\n                    \"anthropic-beta\": \"files-api-2025-04-14\"\n                }\n            }\n        )\n        \n        # The API call should succeed even with beta headers\n        response = agent.run(\"What is 5*5?\")\n        response_text = next((block[\"text\"] for block in response if block.get(\"type\") == \"text\"), \"\")\n        assert \"25\" in response_text\n        self._print(f\"Response with beta headers: {response_text}\")\n        \n    def test_metadata(self) -> None:\n        \"\"\"Test passing valid metadata fields.\"\"\"\n        agent = Agent(\n            name=\"MetadataAgent\",\n            system=\"You are a helpful assistant. Be very brief.\",\n            verbose=False,\n            message_params={\n                \"metadata\": {\n                    \"user_id\": \"test-user-123\"\n                }\n            }\n        )\n        \n        response = agent.run(\"What is 10/2?\")\n        response_text = next((block[\"text\"] for block in response if block.get(\"type\") == \"text\"), \"\")\n        assert \"5\" in response_text\n        self._print(f\"Response with metadata: {response_text}\")\n        \n    def test_api_parameters(self) -> None:\n        \"\"\"Test passing various API parameters.\"\"\"\n        agent = Agent(\n            name=\"ParamsAgent\",\n            system=\"You are a helpful assistant.\",\n            verbose=False,\n            message_params={\n                \"top_k\": 10,\n                \"top_p\": 0.95,\n                \"temperature\": 0.7\n            }\n        )\n        \n        # Verify parameters are passed through\n        params = agent._prepare_message_params()\n        assert params[\"top_k\"] == 10\n        assert params[\"top_p\"] == 0.95\n        assert params[\"temperature\"] == 0.7\n        \n        response = agent.run(\"Say 'test'\")\n        response_text = next((block[\"text\"] for block in response if block.get(\"type\") == \"text\"), \"\")\n        assert response_text\n        self._print(f\"Response with custom params: {response_text}\")\n        \n    def test_parameter_override(self) -> None:\n        \"\"\"Test that message_params override config defaults.\"\"\"\n        config = ModelConfig(\n            temperature=1.0,\n            max_tokens=100\n        )\n        \n        agent = Agent(\n            name=\"OverrideAgent\",\n            system=\"You are a helpful assistant.\",\n            config=config,\n            verbose=False,\n            message_params={\n                \"temperature\": 0.5,  # Should override config\n                \"max_tokens\": 200    # Should override config\n            }\n        )\n        \n        params = agent._prepare_message_params()\n        assert params[\"temperature\"] == 0.5\n        assert params[\"max_tokens\"] == 200\n        self._print(\"Parameter override successful\")\n        \n    def test_invalid_metadata_field(self) -> None:\n        \"\"\"Test that invalid metadata fields are properly rejected by the API.\"\"\"\n        agent = Agent(\n            name=\"InvalidAgent\",\n            system=\"You are a helpful assistant.\",\n            verbose=False,\n            message_params={\n                \"metadata\": {\n                    \"user_id\": \"valid\",\n                    \"invalid_field\": \"should-fail\"\n                }\n            }\n        )\n        \n        try:\n            agent.run(\"Test\")\n            # Should not reach here\n            raise AssertionError(\"Expected API error for invalid metadata field\")\n        except Exception as e:\n            assert \"invalid_request_error\" in str(e) or \"metadata\" in str(e).lower()\n            self._print(f\"Correctly rejected invalid metadata: {type(e).__name__}\")\n            \n    def test_combined_parameters(self) -> None:\n        \"\"\"Test combining multiple parameter types.\"\"\"\n        agent = Agent(\n            name=\"CombinedAgent\",\n            system=\"You are a helpful assistant. Be very brief.\",\n            verbose=False,\n            message_params={\n                \"extra_headers\": {\n                    \"X-Test\": \"combined\",\n                    \"anthropic-beta\": \"files-api-2025-04-14\"\n                },\n                \"metadata\": {\n                    \"user_id\": \"combined-test\"\n                },\n                \"temperature\": 0.8,\n                \"top_k\": 5\n            }\n        )\n        \n        params = agent._prepare_message_params()\n        assert params[\"extra_headers\"][\"X-Test\"] == \"combined\"\n        assert params[\"metadata\"][\"user_id\"] == \"combined-test\"\n        assert params[\"temperature\"] == 0.8\n        assert params[\"top_k\"] == 5\n        \n        response = agent.run(\"What is 1+1?\")\n        response_text = next((block[\"text\"] for block in response if block.get(\"type\") == \"text\"), \"\")\n        assert \"2\" in response_text\n        self._print(f\"Response with combined params: {response_text}\")\n        \n    def run_all_tests(self) -> None:\n        \"\"\"Run all test cases.\"\"\"\n        self._print(\"\\nAgent message_params Test Suite\")\n        self._print(\"=\"*60)\n        \n        tests = [\n            (\"Basic Agent (No message_params)\", self.test_basic_agent),\n            (\"Custom Headers\", self.test_custom_headers),\n            (\"Beta Feature Headers\", self.test_beta_headers),\n            (\"Valid Metadata\", self.test_metadata),\n            (\"API Parameters\", self.test_api_parameters),\n            (\"Parameter Override\", self.test_parameter_override),\n            (\"Invalid Metadata Field\", self.test_invalid_metadata_field),\n            (\"Combined Parameters\", self.test_combined_parameters),\n        ]\n        \n        for test_name, test_func in tests:\n            self._run_test(test_name, test_func)\n            \n        self._print(f\"\\n{'='*60}\")\n        self._print(f\"Test Results: {self.passed} passed, {self.failed} failed\")\n        self._print(\"=\"*60)\n        \n        return self.failed == 0\n\n\ndef main():\n    \"\"\"Run the test suite.\"\"\"\n    # Check for API key\n    if not os.environ.get(\"ANTHROPIC_API_KEY\"):\n        print(\"Error: Please set ANTHROPIC_API_KEY environment variable\")\n        sys.exit(1)\n        \n    # Run tests\n    test_suite = TestMessageParams(verbose=True)\n    success = test_suite.run_all_tests()\n    \n    # Exit with appropriate code\n    sys.exit(0 if success else 1)\n\n\nif __name__ == \"__main__\":\n    main()"
  },
  {
    "path": "agents/tools/__init__.py",
    "content": "\"\"\"Tools module for agent framework.\"\"\"\n\nfrom .base import Tool\nfrom .code_execution import CodeExecutionServerTool\nfrom .file_tools import FileReadTool, FileWriteTool\nfrom .think import ThinkTool\nfrom .web_search import WebSearchServerTool\n\n__all__ = [\n    \"Tool\",\n    \"CodeExecutionServerTool\",\n    \"FileReadTool\",\n    \"FileWriteTool\",\n    \"ThinkTool\",\n    \"WebSearchServerTool\",\n]\n"
  },
  {
    "path": "agents/tools/base.py",
    "content": "\"\"\"Base tool definitions for the agent framework.\"\"\"\n\nfrom dataclasses import dataclass\nfrom typing import Any\n\n\n@dataclass\nclass Tool:\n    \"\"\"Base class for all agent tools.\"\"\"\n\n    name: str\n    description: str\n    input_schema: dict[str, Any]\n\n    def to_dict(self) -> dict[str, Any]:\n        \"\"\"Convert tool to Claude API format.\"\"\"\n        return {\n            \"name\": self.name,\n            \"description\": self.description,\n            \"input_schema\": self.input_schema,\n        }\n\n    async def execute(self, **kwargs) -> str:\n        \"\"\"Execute the tool with provided parameters.\"\"\"\n        raise NotImplementedError(\n            \"Tool subclasses must implement execute method\"\n        )\n"
  },
  {
    "path": "agents/tools/calculator_mcp.py",
    "content": "#!/usr/bin/env python3\n\n\"\"\"Simple calculator tool for basic math operations.\"\"\"\n\nimport math\n\nfrom mcp.server import FastMCP\n\nmcp = FastMCP(\"Calculator\")\n\n\n@mcp.tool(name=\"calculator\")\ndef calculator(number1: float, number2: float, operator: str) -> str:\n    \"\"\"Performs basic calculations with two numbers.\n\n    Args:\n        number1: First number in the calculation\n        number2: Second number in the calculation\n        operator: Operation symbol to perform (+, -, *, /, ^, sqrt)\n               Note: Only these exact symbols are supported, not words\n\n    Returns:\n        Result of the calculation\n    \"\"\"\n    try:\n        if operator == \"+\":\n            result = number1 + number2\n        elif operator == \"-\":\n            result = number1 - number2\n        elif operator == \"*\":\n            result = number1 * number2\n        elif operator == \"/\":\n            if number2 == 0:\n                return \"Error: Division by zero\"\n            result = number1 / number2\n        elif operator == \"^\":\n            result = number1**number2\n        elif operator == \"sqrt\":\n            if number1 < 0:\n                return \"Error: Cannot take square root of negative number\"\n            result = math.sqrt(number1)\n        else:\n            return f\"Error: Unsupported operator '{operator}'\"\n\n        # Format the result\n        if isinstance(result, float) and result.is_integer():\n            result = int(result)\n\n        return f\"Result: {result}\"\n    except Exception as e:\n        return f\"Error: {str(e)}\"\n\n\nif __name__ == \"__main__\":\n    mcp.run()\n"
  },
  {
    "path": "agents/tools/code_execution.py",
    "content": "\"\"\"Code execution server tool for the agent framework.\"\"\"\n\nfrom dataclasses import dataclass\nfrom typing import Any\n\n\n@dataclass\nclass CodeExecutionServerTool:\n    \"\"\"Code execution server tool that uses Anthropic's server tool format.\"\"\"\n    \n    name: str = \"code_execution\"\n    type: str = \"code_execution_20250522\"\n    \n    def to_dict(self) -> dict[str, Any]:\n        \"\"\"Convert to Anthropic server tool format.\"\"\"\n        return {\n            \"type\": self.type,\n            \"name\": self.name,\n        }"
  },
  {
    "path": "agents/tools/file_tools.py",
    "content": "\"\"\"File operation tools for reading and writing files.\"\"\"\n\nimport asyncio\nimport glob\nimport os\nfrom pathlib import Path\n\nfrom .base import Tool\n\n\nclass FileReadTool(Tool):\n    \"\"\"Tool for reading files and listing directories.\"\"\"\n\n    def __init__(self):\n        super().__init__(\n            name=\"file_read\",\n            description=\"\"\"\n            Read files or list directory contents.\n\n            Operations:\n            - read: Read the contents of a file\n            - list: List files in a directory\n            \"\"\",\n            input_schema={\n                \"type\": \"object\",\n                \"properties\": {\n                    \"operation\": {\n                        \"type\": \"string\",\n                        \"enum\": [\"read\", \"list\"],\n                        \"description\": \"File operation to perform\",\n                    },\n                    \"path\": {\n                        \"type\": \"string\",\n                        \"description\": \"File path for read or directory path\",\n                    },\n                    \"max_lines\": {\n                        \"type\": \"integer\",\n                        \"description\": \"Maximum lines to read (0 means no limit)\",\n                    },\n                    \"pattern\": {\n                        \"type\": \"string\",\n                        \"description\": \"File pattern to match\",\n                    },\n                },\n                \"required\": [\"operation\", \"path\"],\n            },\n        )\n\n    async def execute(\n        self,\n        operation: str,\n        path: str,\n        max_lines: int = 0,\n        pattern: str = \"*\",\n    ) -> str:\n        \"\"\"Execute a file read operation.\n\n        Args:\n            operation: The operation to perform (read or list)\n            path: The file or directory path\n            max_lines: Maximum lines to read (for read operation, 0 means no limit)\n            pattern: File pattern to match (for list operation)\n\n        Returns:\n            Result of the operation as string\n        \"\"\"\n        if operation == \"read\":\n            return await self._read_file(path, max_lines)\n        elif operation == \"list\":\n            return await self._list_files(path, pattern)\n        else:\n            return f\"Error: Unsupported operation '{operation}'\"\n\n    async def _read_file(self, path: str, max_lines: int = 0) -> str:\n        \"\"\"Read a file from disk.\n        \n        Args:\n            path: Path to the file to read\n            max_lines: Maximum number of lines to read (0 means read entire file)\n        \"\"\"\n        try:\n            file_path = Path(path)\n\n            if not file_path.exists():\n                return f\"Error: File not found at {path}\"\n            if not file_path.is_file():\n                return f\"Error: {path} is not a file\"\n\n            def read_sync():\n                with open(file_path, encoding=\"utf-8\", errors=\"replace\") as f:\n                    if max_lines > 0:\n                        lines = []\n                        for i, line in enumerate(f):\n                            if i >= max_lines:\n                                break\n                            lines.append(line)\n                        return \"\".join(lines)\n                    return f.read()\n\n            return await asyncio.to_thread(read_sync)\n        except Exception as e:\n            return f\"Error reading {path}: {str(e)}\"\n\n    async def _list_files(self, directory: str, pattern: str = \"*\") -> str:\n        \"\"\"List files in a directory.\"\"\"\n        try:\n            dir_path = Path(directory)\n\n            if not dir_path.exists():\n                return f\"Error: Directory not found at {directory}\"\n            if not dir_path.is_dir():\n                return f\"Error: {directory} is not a directory\"\n\n            def list_sync():\n                search_pattern = f\"{directory}/{pattern}\"\n                files = glob.glob(search_pattern)\n\n                if not files:\n                    return f\"No files found matching {directory}/{pattern}\"\n\n                file_list = []\n                for file_path in sorted(files):\n                    path_obj = Path(file_path)\n                    rel_path = str(file_path).replace(str(dir_path) + \"/\", \"\")\n\n                    if path_obj.is_dir():\n                        file_list.append(f\"📁 {rel_path}/\")\n                    else:\n                        file_list.append(f\"📄 {rel_path}\")\n\n                return \"\\n\".join(file_list)\n\n            return await asyncio.to_thread(list_sync)\n        except Exception as e:\n            return f\"Error listing files in {directory}: {str(e)}\"\n\n\nclass FileWriteTool(Tool):\n    \"\"\"Tool for writing and editing files.\"\"\"\n\n    def __init__(self):\n        super().__init__(\n            name=\"file_write\",\n            description=\"\"\"\n            Write or edit files.\n\n            Operations:\n            - write: Create or completely replace a file\n            - edit: Make targeted changes to parts of a file\n            \"\"\",\n            input_schema={\n                \"type\": \"object\",\n                \"properties\": {\n                    \"operation\": {\n                        \"type\": \"string\",\n                        \"enum\": [\"write\", \"edit\"],\n                        \"description\": \"File operation to perform\",\n                    },\n                    \"path\": {\n                        \"type\": \"string\",\n                        \"description\": \"File path to write to or edit\",\n                    },\n                    \"content\": {\n                        \"type\": \"string\",\n                        \"description\": \"Content to write\",\n                    },\n                    \"old_text\": {\n                        \"type\": \"string\",\n                        \"description\": \"Text to replace (for edit operation)\",\n                    },\n                    \"new_text\": {\n                        \"type\": \"string\",\n                        \"description\": \"Replacement text (for edit operation)\",\n                    },\n                },\n                \"required\": [\"operation\", \"path\"],\n            },\n        )\n\n    async def execute(\n        self,\n        operation: str,\n        path: str,\n        content: str = \"\",\n        old_text: str = \"\",\n        new_text: str = \"\",\n    ) -> str:\n        \"\"\"Execute a file write operation.\n\n        Args:\n            operation: The operation to perform (write or edit)\n            path: The file path\n            content: Content to write (for write operation)\n            old_text: Text to replace (for edit operation)\n            new_text: Replacement text (for edit operation)\n\n        Returns:\n            Result of the operation as string\n        \"\"\"\n        if operation == \"write\":\n            if not content:\n                return \"Error: content parameter is required\"\n            return await self._write_file(path, content)\n        elif operation == \"edit\":\n            if not old_text or not new_text:\n                return (\n                    \"Error: both old_text and new_text parameters \"\n                    \"are required for edit operation\"\n                )\n            return await self._edit_file(path, old_text, new_text)\n        else:\n            return f\"Error: Unsupported operation '{operation}'\"\n\n    async def _write_file(self, path: str, content: str) -> str:\n        \"\"\"Write content to a file.\"\"\"\n        try:\n            file_path = Path(path)\n            os.makedirs(file_path.parent, exist_ok=True)\n\n            def write_sync():\n                with open(file_path, \"w\", encoding=\"utf-8\") as f:\n                    f.write(content)\n                return (\n                    f\"Successfully wrote {len(content)} \"\n                    f\"characters to {path}\"\n                )\n\n            return await asyncio.to_thread(write_sync)\n        except Exception as e:\n            return f\"Error writing to {path}: {str(e)}\"\n\n    async def _edit_file(self, path: str, old_text: str, new_text: str) -> str:\n        \"\"\"Make targeted changes to a file.\"\"\"\n        try:\n            file_path = Path(path)\n\n            if not file_path.exists():\n                return f\"Error: File not found at {path}\"\n            if not file_path.is_file():\n                return f\"Error: {path} is not a file\"\n\n            def edit_sync():\n                try:\n                    with open(\n                        file_path, encoding=\"utf-8\", errors=\"replace\"\n                    ) as f:\n                        content = f.read()\n\n                    if old_text not in content:\n                        return (\n                            f\"Error: The specified text was not \"\n                            f\"found in {path}\"\n                        )\n\n                    # Count occurrences to warn about multiple matches\n                    count = content.count(old_text)\n                    if count > 1:\n                        # Edit with warning about multiple occurrences\n                        new_content = content.replace(old_text, new_text)\n                        with open(file_path, \"w\", encoding=\"utf-8\") as f:\n                            f.write(new_content)\n                        return (\n                            f\"Warning: Found {count} occurrences. \"\n                            f\"All were replaced in {path}\"\n                        )\n                    else:\n                        # One occurrence, straightforward replacement\n                        new_content = content.replace(old_text, new_text)\n                        with open(file_path, \"w\", encoding=\"utf-8\") as f:\n                            f.write(new_content)\n                        return f\"Successfully edited {path}\"\n                except UnicodeDecodeError:\n                    return f\"Error: {path} appears to be a binary file\"\n\n            return await asyncio.to_thread(edit_sync)\n        except Exception as e:\n            return f\"Error editing {path}: {str(e)}\"\n"
  },
  {
    "path": "agents/tools/mcp_tool.py",
    "content": "\"\"\"Tools that interface with MCP servers.\"\"\"\n\nfrom typing import Any\nfrom .base import Tool\nfrom ..utils.connections import MCPConnection\n\n\nclass MCPTool(Tool):\n    def __init__(\n        self,\n        name: str,\n        description: str,\n        input_schema: dict[str, Any],\n        connection: \"MCPConnection\",\n    ):\n        super().__init__(\n            name=name, description=description, input_schema=input_schema\n        )\n        self.connection = connection\n\n    async def execute(self, **kwargs) -> str:\n        \"\"\"Execute the MCP tool with the given input_schema.\n        Note: Currently only supports text results from MCP tools.\"\"\"\n        try:\n            result = await self.connection.call_tool(\n                self.name, arguments=kwargs\n            )\n\n            if hasattr(result, \"content\") and result.content:\n                for item in result.content:\n                    if getattr(item, \"type\", None) == \"text\":\n                        return item.text\n\n            return \"No text content in tool response\"\n        except Exception as e:\n            return f\"Error executing {self.name}: {e}\"\n"
  },
  {
    "path": "agents/tools/think.py",
    "content": "\"\"\"Think tool for internal reasoning.\"\"\"\n\nfrom .base import Tool\n\n\nclass ThinkTool(Tool):\n    \"\"\"Tool for internal reasoning without executing external actions.\"\"\"\n\n    def __init__(self):\n        super().__init__(\n            name=\"think\",\n            description=(\n                \"Use the tool to think about something. It will not obtain \"\n                \"new information or change the database, but just append the \"\n                \"thought to the log. Use it when complex reasoning or some \"\n                \"cache memory is needed.\"\n            ),\n            input_schema={\n                \"type\": \"object\",\n                \"properties\": {\n                    \"thought\": {\n                        \"type\": \"string\",\n                        \"description\": \"A thought to think about.\",\n                    }\n                },\n                \"required\": [\"thought\"],\n            },\n        )\n\n    async def execute(self, thought: str) -> str:\n        \"\"\"Simply returns the thought back to the model.\"\"\"\n        return \"Thinking complete!\"\n"
  },
  {
    "path": "agents/tools/web_search.py",
    "content": "\"\"\"Web search server tool for the agent framework.\"\"\"\n\nfrom dataclasses import dataclass\nfrom typing import Any, Optional\n\n\n@dataclass\nclass WebSearchServerTool:\n    \"\"\"Web search server tool that uses Anthropic's server tool format.\"\"\"\n    \n    name: str = \"web_search\"\n    type: str = \"web_search_20250305\"\n    max_uses: Optional[int] = None\n    allowed_domains: Optional[list[str]] = None\n    blocked_domains: Optional[list[str]] = None\n    user_location: Optional[dict[str, Any]] = None\n    \n    def to_dict(self) -> dict[str, Any]:\n        \"\"\"Convert to Anthropic server tool format.\"\"\"\n        tool_dict: dict[str, Any] = {\n            \"type\": self.type,\n            \"name\": self.name,\n        }\n        \n        # Add optional parameters if provided\n        if self.max_uses is not None:\n            tool_dict[\"max_uses\"] = self.max_uses\n            \n        if self.allowed_domains is not None:\n            tool_dict[\"allowed_domains\"] = self.allowed_domains\n            \n        if self.blocked_domains is not None:\n            tool_dict[\"blocked_domains\"] = self.blocked_domains\n            \n        if self.user_location is not None:\n            tool_dict[\"user_location\"] = self.user_location\n            \n        return tool_dict"
  },
  {
    "path": "agents/utils/__init__.py",
    "content": "\"\"\"Agent utility modules.\"\"\"\n\nfrom .history_util import MessageHistory\nfrom .tool_util import execute_tools\n\n__all__ = [\"MessageHistory\", \"execute_tools\"]\n"
  },
  {
    "path": "agents/utils/connections.py",
    "content": "\"\"\"Connection handling for MCP servers.\"\"\"\n\nfrom abc import ABC, abstractmethod\nfrom contextlib import AsyncExitStack\nfrom typing import Any\n\nfrom mcp import ClientSession, StdioServerParameters\nfrom mcp.client.sse import sse_client\nfrom mcp.client.stdio import stdio_client\n\nfrom ..tools.mcp_tool import MCPTool\n\n\nclass MCPConnection(ABC):\n    \"\"\"Base class for MCP server connections.\"\"\"\n\n    def __init__(self):\n        self.session = None\n        self._rw_ctx = None\n        self._session_ctx = None\n\n    @abstractmethod\n    async def _create_rw_context(self):\n        \"\"\"Create the read/write context based on connection type.\"\"\"\n\n    async def __aenter__(self):\n        \"\"\"Initialize MCP server connection.\"\"\"\n        self._rw_ctx = await self._create_rw_context()\n        read_write = await self._rw_ctx.__aenter__()\n        read, write = read_write\n        self._session_ctx = ClientSession(read, write)\n        self.session = await self._session_ctx.__aenter__()\n        await self.session.initialize()\n        return self\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        \"\"\"Clean up MCP server connection resources.\"\"\"\n        try:\n            if self._session_ctx:\n                await self._session_ctx.__aexit__(exc_type, exc_val, exc_tb)\n            if self._rw_ctx:\n                await self._rw_ctx.__aexit__(exc_type, exc_val, exc_tb)\n        except Exception as e:\n            print(f\"Error during cleanup: {e}\")\n        finally:\n            self.session = None\n            self._session_ctx = None\n            self._rw_ctx = None\n\n    async def list_tools(self) -> Any:\n        \"\"\"Retrieve available tools from the MCP server.\"\"\"\n        response = await self.session.list_tools()\n        return response.tools\n\n    async def call_tool(\n        self, tool_name: str, arguments: dict[str, Any]\n    ) -> Any:\n        \"\"\"Call a tool on the MCP server with provided arguments.\"\"\"\n        return await self.session.call_tool(tool_name, arguments=arguments)\n\n\nclass MCPConnectionStdio(MCPConnection):\n    \"\"\"MCP connection using standard input/output.\"\"\"\n\n    def __init__(\n        self, command: str, args: list[str] = [], env: dict[str, str] = None\n    ):\n        super().__init__()\n        self.command = command\n        self.args = args\n        self.env = env\n\n    async def _create_rw_context(self):\n        return stdio_client(\n            StdioServerParameters(\n                command=self.command, args=self.args, env=self.env\n            )\n        )\n\n\nclass MCPConnectionSSE(MCPConnection):\n    \"\"\"MCP connection using Server-Sent Events.\"\"\"\n\n    def __init__(self, url: str, headers: dict[str, str] = None):\n        super().__init__()\n        self.url = url\n        self.headers = headers or {}\n\n    async def _create_rw_context(self):\n        return sse_client(url=self.url, headers=self.headers)\n\n\ndef create_mcp_connection(config: dict[str, Any]) -> MCPConnection:\n    \"\"\"Factory function to create the appropriate MCP connection.\"\"\"\n    conn_type = config.get(\"type\", \"stdio\").lower()\n\n    if conn_type == \"stdio\":\n        if not config.get(\"command\"):\n            raise ValueError(\"Command is required for STDIO connections\")\n        return MCPConnectionStdio(\n            command=config[\"command\"],\n            args=config.get(\"args\"),\n            env=config.get(\"env\"),\n        )\n\n    elif conn_type == \"sse\":\n        if not config.get(\"url\"):\n            raise ValueError(\"URL is required for SSE connections\")\n        return MCPConnectionSSE(\n            url=config[\"url\"], headers=config.get(\"headers\")\n        )\n\n    else:\n        raise ValueError(f\"Unsupported connection type: {conn_type}\")\n\n\nasync def setup_mcp_connections(\n    mcp_servers: list[dict[str, Any]] | None,\n    stack: AsyncExitStack,\n) -> list[MCPTool]:\n    \"\"\"Set up MCP server connections and create tool interfaces.\"\"\"\n    if not mcp_servers:\n        return []\n\n    mcp_tools = []\n\n    for config in mcp_servers:\n        try:\n            connection = create_mcp_connection(config)\n            await stack.enter_async_context(connection)\n            tool_definitions = await connection.list_tools()\n\n            for tool_info in tool_definitions:\n                mcp_tools.append(\n                    MCPTool(\n                        name=tool_info.name,\n                        description=tool_info.description\n                        or f\"MCP tool: {tool_info.name}\",\n                        input_schema=tool_info.inputSchema,\n                        connection=connection,\n                    )\n                )\n\n        except Exception as e:\n            print(f\"Error setting up MCP server {config}: {e}\")\n\n    print(\n        f\"Loaded {len(mcp_tools)} MCP tools from {len(mcp_servers)} servers.\"\n    )\n    return mcp_tools\n"
  },
  {
    "path": "agents/utils/history_util.py",
    "content": "\"\"\"Message history with token tracking and prompt caching.\"\"\"\n\nfrom typing import Any\n\n\nclass MessageHistory:\n    \"\"\"Manages chat history with token tracking and context management.\"\"\"\n\n    def __init__(\n        self,\n        model: str,\n        system: str,\n        context_window_tokens: int,\n        client: Any,\n        enable_caching: bool = True,\n    ):\n        self.model = model\n        self.system = system\n        self.context_window_tokens = context_window_tokens\n        self.messages: list[dict[str, Any]] = []\n        self.total_tokens = 0\n        self.enable_caching = enable_caching\n        self.message_tokens: list[tuple[int, int]] = (\n            []\n        )  # List of (input_tokens, output_tokens) tuples\n        self.client = client\n\n        # set initial total tokens to system prompt\n        try:\n            system_token = (\n                self.client.messages.count_tokens(\n                    model=self.model,\n                    system=self.system,\n                    messages=[{\"role\": \"user\", \"content\": \"test\"}],\n                ).input_tokens\n                - 1\n            )\n\n        except Exception:\n            system_token = len(self.system) / 4\n\n        self.total_tokens = system_token\n\n    async def add_message(\n        self,\n        role: str,\n        content: str | list[dict[str, Any]],\n        usage: Any | None = None,\n    ):\n        \"\"\"Add a message to the history and track token usage.\"\"\"\n        if isinstance(content, str):\n            content = [{\"type\": \"text\", \"text\": content}]\n\n        message = {\"role\": role, \"content\": content}\n        self.messages.append(message)\n\n        if role == \"assistant\" and usage:\n            total_input = (\n                usage.input_tokens\n                + getattr(usage, \"cache_read_input_tokens\", 0)\n                + getattr(usage, \"cache_creation_input_tokens\", 0)\n            )\n            output_tokens = usage.output_tokens\n\n            current_turn_input = total_input - self.total_tokens\n            self.message_tokens.append((current_turn_input, output_tokens))\n            self.total_tokens += current_turn_input + output_tokens\n\n    def truncate(self) -> None:\n        \"\"\"Remove oldest messages when context window limit is exceeded.\"\"\"\n        if self.total_tokens <= self.context_window_tokens:\n            return\n\n        TRUNCATION_NOTICE_TOKENS = 25\n        TRUNCATION_MESSAGE = {\n            \"role\": \"user\",\n            \"content\": [\n                {\n                    \"type\": \"text\",\n                    \"text\": \"[Earlier history has been truncated.]\",\n                }\n            ],\n        }\n\n        def remove_message_pair():\n            self.messages.pop(0)\n            self.messages.pop(0)\n\n            if self.message_tokens:\n                input_tokens, output_tokens = self.message_tokens.pop(0)\n                self.total_tokens -= input_tokens + output_tokens\n\n        while (\n            self.message_tokens\n            and len(self.messages) >= 2\n            and self.total_tokens > self.context_window_tokens\n        ):\n            remove_message_pair()\n\n            if self.messages and self.message_tokens:\n                original_input_tokens, original_output_tokens = (\n                    self.message_tokens[0]\n                )\n                self.messages[0] = TRUNCATION_MESSAGE\n                self.message_tokens[0] = (\n                    TRUNCATION_NOTICE_TOKENS,\n                    original_output_tokens,\n                )\n                self.total_tokens += (\n                    TRUNCATION_NOTICE_TOKENS - original_input_tokens\n                )\n\n    def format_for_api(self) -> list[dict[str, Any]]:\n        \"\"\"Format messages for Claude API with optional caching.\"\"\"\n        result = [\n            {\"role\": m[\"role\"], \"content\": m[\"content\"]} for m in self.messages\n        ]\n\n        if self.enable_caching and self.messages:\n            result[-1][\"content\"] = [\n                {**block, \"cache_control\": {\"type\": \"ephemeral\"}}\n                for block in self.messages[-1][\"content\"]\n            ]\n        return result\n"
  },
  {
    "path": "agents/utils/tool_util.py",
    "content": "\"\"\"Tool execution utility with parallel execution support.\"\"\"\n\nimport asyncio\nfrom typing import Any\n\n\nasync def _execute_single_tool(\n    call: Any, tool_dict: dict[str, Any]\n) -> dict[str, Any]:\n    \"\"\"Execute a single tool and handle errors.\"\"\"\n    response = {\"type\": \"tool_result\", \"tool_use_id\": call.id}\n\n    try:\n        # Execute the tool directly\n        result = await tool_dict[call.name].execute(**call.input)\n        response[\"content\"] = str(result)\n    except KeyError:\n        response[\"content\"] = f\"Tool '{call.name}' not found\"\n        response[\"is_error\"] = True\n    except Exception as e:\n        response[\"content\"] = f\"Error executing tool: {str(e)}\"\n        response[\"is_error\"] = True\n\n    return response\n\n\nasync def execute_tools(\n    tool_calls: list[Any], tool_dict: dict[str, Any], parallel: bool = True\n) -> list[dict[str, Any]]:\n    \"\"\"Execute multiple tools sequentially or in parallel.\"\"\"\n\n    if parallel:\n        return await asyncio.gather(\n            *[_execute_single_tool(call, tool_dict) for call in tool_calls]\n        )\n    else:\n        return [\n            await _execute_single_tool(call, tool_dict) for call in tool_calls\n        ]\n"
  },
  {
    "path": "autonomous-coding/.gitignore",
    "content": "# Agent-generated output directories\ngenerations/\n\n# Log files\nlogs/\n"
  },
  {
    "path": "autonomous-coding/README.md",
    "content": "# Autonomous Coding Agent Demo\n\nA minimal harness demonstrating long-running autonomous coding with the Claude Agent SDK. This demo implements a two-agent pattern (initializer + coding agent) that can build complete applications over multiple sessions.\n\n## Prerequisites\n\n**Required:** Install the latest versions of both Claude Code and the Claude Agent SDK:\n\n```bash\n# Install Claude Code CLI (latest version required)\nnpm install -g @anthropic-ai/claude-code\n\n# Install Python dependencies\npip install -r requirements.txt\n```\n\nVerify your installations:\n```bash\nclaude --version  # Should be latest version\npip show claude-code-sdk  # Check SDK is installed\n```\n\n**API Key:** Set your Anthropic API key:\n```bash\nexport ANTHROPIC_API_KEY='your-api-key-here'\n```\n\n## Quick Start\n\n```bash\npython autonomous_agent_demo.py --project-dir ./my_project\n```\n\nFor testing with limited iterations:\n```bash\npython autonomous_agent_demo.py --project-dir ./my_project --max-iterations 3\n```\n\n## Important Timing Expectations\n\n> **Warning: This demo takes a long time to run!**\n\n- **First session (initialization):** The agent generates a `feature_list.json` with 200 test cases. This takes several minutes and may appear to hang - this is normal. The agent is writing out all the features.\n\n- **Subsequent sessions:** Each coding iteration can take **5-15 minutes** depending on complexity.\n\n- **Full app:** Building all 200 features typically requires **many hours** of total runtime across multiple sessions.\n\n**Tip:** The 200 features parameter in the prompts is designed for comprehensive coverage. If you want faster demos, you can modify `prompts/initializer_prompt.md` to reduce the feature count (e.g., 20-50 features for a quicker demo).\n\n## How It Works\n\n### Two-Agent Pattern\n\n1. **Initializer Agent (Session 1):** Reads `app_spec.txt`, creates `feature_list.json` with 200 test cases, sets up project structure, and initializes git.\n\n2. **Coding Agent (Sessions 2+):** Picks up where the previous session left off, implements features one by one, and marks them as passing in `feature_list.json`.\n\n### Session Management\n\n- Each session runs with a fresh context window\n- Progress is persisted via `feature_list.json` and git commits\n- The agent auto-continues between sessions (3 second delay)\n- Press `Ctrl+C` to pause; run the same command to resume\n\n## Security Model\n\nThis demo uses a defense-in-depth security approach (see `security.py` and `client.py`):\n\n1. **OS-level Sandbox:** Bash commands run in an isolated environment\n2. **Filesystem Restrictions:** File operations restricted to the project directory only\n3. **Bash Allowlist:** Only specific commands are permitted:\n   - File inspection: `ls`, `cat`, `head`, `tail`, `wc`, `grep`\n   - Node.js: `npm`, `node`\n   - Version control: `git`\n   - Process management: `ps`, `lsof`, `sleep`, `pkill` (dev processes only)\n\nCommands not in the allowlist are blocked by the security hook.\n\n## Project Structure\n\n```\nautonomous-coding/\n├── autonomous_agent_demo.py  # Main entry point\n├── agent.py                  # Agent session logic\n├── client.py                 # Claude SDK client configuration\n├── security.py               # Bash command allowlist and validation\n├── progress.py               # Progress tracking utilities\n├── prompts.py                # Prompt loading utilities\n├── prompts/\n│   ├── app_spec.txt          # Application specification\n│   ├── initializer_prompt.md # First session prompt\n│   └── coding_prompt.md      # Continuation session prompt\n└── requirements.txt          # Python dependencies\n```\n\n## Generated Project Structure\n\nAfter running, your project directory will contain:\n\n```\nmy_project/\n├── feature_list.json         # Test cases (source of truth)\n├── app_spec.txt              # Copied specification\n├── init.sh                   # Environment setup script\n├── claude-progress.txt       # Session progress notes\n├── .claude_settings.json     # Security settings\n└── [application files]       # Generated application code\n```\n\n## Running the Generated Application\n\nAfter the agent completes (or pauses), you can run the generated application:\n\n```bash\ncd generations/my_project\n\n# Run the setup script created by the agent\n./init.sh\n\n# Or manually (typical for Node.js apps):\nnpm install\nnpm run dev\n```\n\nThe application will typically be available at `http://localhost:3000` or similar (check the agent's output or `init.sh` for the exact URL).\n\n## Command Line Options\n\n| Option | Description | Default |\n|--------|-------------|---------|\n| `--project-dir` | Directory for the project | `./autonomous_demo_project` |\n| `--max-iterations` | Max agent iterations | Unlimited |\n| `--model` | Claude model to use | `claude-sonnet-4-5-20250929` |\n\n## Customization\n\n### Changing the Application\n\nEdit `prompts/app_spec.txt` to specify a different application to build.\n\n### Adjusting Feature Count\n\nEdit `prompts/initializer_prompt.md` and change the \"200 features\" requirement to a smaller number for faster demos.\n\n### Modifying Allowed Commands\n\nEdit `security.py` to add or remove commands from `ALLOWED_COMMANDS`.\n\n## Troubleshooting\n\n**\"Appears to hang on first run\"**\nThis is normal. The initializer agent is generating 200 detailed test cases, which takes significant time. Watch for `[Tool: ...]` output to confirm the agent is working.\n\n**\"Command blocked by security hook\"**\nThe agent tried to run a command not in the allowlist. This is the security system working as intended. If needed, add the command to `ALLOWED_COMMANDS` in `security.py`.\n\n**\"API key not set\"**\nEnsure `ANTHROPIC_API_KEY` is exported in your shell environment.\n\n## License\n\nInternal Anthropic use.\n"
  },
  {
    "path": "autonomous-coding/agent.py",
    "content": "\"\"\"\nAgent Session Logic\n===================\n\nCore agent interaction functions for running autonomous coding sessions.\n\"\"\"\n\nimport asyncio\nfrom pathlib import Path\nfrom typing import Optional\n\nfrom claude_code_sdk import ClaudeSDKClient\n\nfrom client import create_client\nfrom progress import print_session_header, print_progress_summary\nfrom prompts import get_initializer_prompt, get_coding_prompt, copy_spec_to_project\n\n\n# Configuration\nAUTO_CONTINUE_DELAY_SECONDS = 3\n\n\nasync def run_agent_session(\n    client: ClaudeSDKClient,\n    message: str,\n    project_dir: Path,\n) -> tuple[str, str]:\n    \"\"\"\n    Run a single agent session using Claude Agent SDK.\n\n    Args:\n        client: Claude SDK client\n        message: The prompt to send\n        project_dir: Project directory path\n\n    Returns:\n        (status, response_text) where status is:\n        - \"continue\" if agent should continue working\n        - \"error\" if an error occurred\n    \"\"\"\n    print(\"Sending prompt to Claude Agent SDK...\\n\")\n\n    try:\n        # Send the query\n        await client.query(message)\n\n        # Collect response text and show tool use\n        response_text = \"\"\n        async for msg in client.receive_response():\n            msg_type = type(msg).__name__\n\n            # Handle AssistantMessage (text and tool use)\n            if msg_type == \"AssistantMessage\" and hasattr(msg, \"content\"):\n                for block in msg.content:\n                    block_type = type(block).__name__\n\n                    if block_type == \"TextBlock\" and hasattr(block, \"text\"):\n                        response_text += block.text\n                        print(block.text, end=\"\", flush=True)\n                    elif block_type == \"ToolUseBlock\" and hasattr(block, \"name\"):\n                        print(f\"\\n[Tool: {block.name}]\", flush=True)\n                        if hasattr(block, \"input\"):\n                            input_str = str(block.input)\n                            if len(input_str) > 200:\n                                print(f\"   Input: {input_str[:200]}...\", flush=True)\n                            else:\n                                print(f\"   Input: {input_str}\", flush=True)\n\n            # Handle UserMessage (tool results)\n            elif msg_type == \"UserMessage\" and hasattr(msg, \"content\"):\n                for block in msg.content:\n                    block_type = type(block).__name__\n\n                    if block_type == \"ToolResultBlock\":\n                        result_content = getattr(block, \"content\", \"\")\n                        is_error = getattr(block, \"is_error\", False)\n\n                        # Check if command was blocked by security hook\n                        if \"blocked\" in str(result_content).lower():\n                            print(f\"   [BLOCKED] {result_content}\", flush=True)\n                        elif is_error:\n                            # Show errors (truncated)\n                            error_str = str(result_content)[:500]\n                            print(f\"   [Error] {error_str}\", flush=True)\n                        else:\n                            # Tool succeeded - just show brief confirmation\n                            print(\"   [Done]\", flush=True)\n\n        print(\"\\n\" + \"-\" * 70 + \"\\n\")\n        return \"continue\", response_text\n\n    except Exception as e:\n        print(f\"Error during agent session: {e}\")\n        return \"error\", str(e)\n\n\nasync def run_autonomous_agent(\n    project_dir: Path,\n    model: str,\n    max_iterations: Optional[int] = None,\n) -> None:\n    \"\"\"\n    Run the autonomous agent loop.\n\n    Args:\n        project_dir: Directory for the project\n        model: Claude model to use\n        max_iterations: Maximum number of iterations (None for unlimited)\n    \"\"\"\n    print(\"\\n\" + \"=\" * 70)\n    print(\"  AUTONOMOUS CODING AGENT DEMO\")\n    print(\"=\" * 70)\n    print(f\"\\nProject directory: {project_dir}\")\n    print(f\"Model: {model}\")\n    if max_iterations:\n        print(f\"Max iterations: {max_iterations}\")\n    else:\n        print(\"Max iterations: Unlimited (will run until completion)\")\n    print()\n\n    # Create project directory\n    project_dir.mkdir(parents=True, exist_ok=True)\n\n    # Check if this is a fresh start or continuation\n    tests_file = project_dir / \"feature_list.json\"\n    is_first_run = not tests_file.exists()\n\n    if is_first_run:\n        print(\"Fresh start - will use initializer agent\")\n        print()\n        print(\"=\" * 70)\n        print(\"  NOTE: First session takes 10-20+ minutes!\")\n        print(\"  The agent is generating 200 detailed test cases.\")\n        print(\"  This may appear to hang - it's working. Watch for [Tool: ...] output.\")\n        print(\"=\" * 70)\n        print()\n        # Copy the app spec into the project directory for the agent to read\n        copy_spec_to_project(project_dir)\n    else:\n        print(\"Continuing existing project\")\n        print_progress_summary(project_dir)\n\n    # Main loop\n    iteration = 0\n\n    while True:\n        iteration += 1\n\n        # Check max iterations\n        if max_iterations and iteration > max_iterations:\n            print(f\"\\nReached max iterations ({max_iterations})\")\n            print(\"To continue, run the script again without --max-iterations\")\n            break\n\n        # Print session header\n        print_session_header(iteration, is_first_run)\n\n        # Create client (fresh context)\n        client = create_client(project_dir, model)\n\n        # Choose prompt based on session type\n        if is_first_run:\n            prompt = get_initializer_prompt()\n            is_first_run = False  # Only use initializer once\n        else:\n            prompt = get_coding_prompt()\n\n        # Run session with async context manager\n        async with client:\n            status, response = await run_agent_session(client, prompt, project_dir)\n\n        # Handle status\n        if status == \"continue\":\n            print(f\"\\nAgent will auto-continue in {AUTO_CONTINUE_DELAY_SECONDS}s...\")\n            print_progress_summary(project_dir)\n            await asyncio.sleep(AUTO_CONTINUE_DELAY_SECONDS)\n\n        elif status == \"error\":\n            print(\"\\nSession encountered an error\")\n            print(\"Will retry with a fresh session...\")\n            await asyncio.sleep(AUTO_CONTINUE_DELAY_SECONDS)\n\n        # Small delay between sessions\n        if max_iterations is None or iteration < max_iterations:\n            print(\"\\nPreparing next session...\\n\")\n            await asyncio.sleep(1)\n\n    # Final summary\n    print(\"\\n\" + \"=\" * 70)\n    print(\"  SESSION COMPLETE\")\n    print(\"=\" * 70)\n    print(f\"\\nProject directory: {project_dir}\")\n    print_progress_summary(project_dir)\n\n    # Print instructions for running the generated application\n    print(\"\\n\" + \"-\" * 70)\n    print(\"  TO RUN THE GENERATED APPLICATION:\")\n    print(\"-\" * 70)\n    print(f\"\\n  cd {project_dir.resolve()}\")\n    print(\"  ./init.sh           # Run the setup script\")\n    print(\"  # Or manually:\")\n    print(\"  npm install && npm run dev\")\n    print(\"\\n  Then open http://localhost:3000 (or check init.sh for the URL)\")\n    print(\"-\" * 70)\n\n    print(\"\\nDone!\")\n"
  },
  {
    "path": "autonomous-coding/autonomous_agent_demo.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nAutonomous Coding Agent Demo\n============================\n\nA minimal harness demonstrating long-running autonomous coding with Claude.\nThis script implements the two-agent pattern (initializer + coding agent) and\nincorporates all the strategies from the long-running agents guide.\n\nExample Usage:\n    python autonomous_agent_demo.py --project-dir ./claude_clone_demo\n    python autonomous_agent_demo.py --project-dir ./claude_clone_demo --max-iterations 5\n\"\"\"\n\nimport argparse\nimport asyncio\nimport os\nfrom pathlib import Path\n\nfrom agent import run_autonomous_agent\n\n\n# Configuration\nDEFAULT_MODEL = \"claude-sonnet-4-5-20250929\"\n\n\ndef parse_args() -> argparse.Namespace:\n    \"\"\"Parse command line arguments.\"\"\"\n    parser = argparse.ArgumentParser(\n        description=\"Autonomous Coding Agent Demo - Long-running agent harness\",\n        formatter_class=argparse.RawDescriptionHelpFormatter,\n        epilog=\"\"\"\nExamples:\n  # Start fresh project\n  python autonomous_agent_demo.py --project-dir ./claude_clone\n\n  # Use a specific model\n  python autonomous_agent_demo.py --project-dir ./claude_clone --model claude-sonnet-4-5-20250929\n\n  # Limit iterations for testing\n  python autonomous_agent_demo.py --project-dir ./claude_clone --max-iterations 5\n\n  # Continue existing project\n  python autonomous_agent_demo.py --project-dir ./claude_clone\n\nEnvironment Variables:\n  ANTHROPIC_API_KEY    Your Anthropic API key (required)\n        \"\"\",\n    )\n\n    parser.add_argument(\n        \"--project-dir\",\n        type=Path,\n        default=Path(\"./autonomous_demo_project\"),\n        help=\"Directory for the project (default: generations/autonomous_demo_project). Relative paths automatically placed in generations/ directory.\",\n    )\n\n    parser.add_argument(\n        \"--max-iterations\",\n        type=int,\n        default=None,\n        help=\"Maximum number of agent iterations (default: unlimited)\",\n    )\n\n    parser.add_argument(\n        \"--model\",\n        type=str,\n        default=DEFAULT_MODEL,\n        help=f\"Claude model to use (default: {DEFAULT_MODEL})\",\n    )\n\n    return parser.parse_args()\n\n\ndef main() -> None:\n    \"\"\"Main entry point.\"\"\"\n    args = parse_args()\n\n    # Check for API key\n    if not os.environ.get(\"ANTHROPIC_API_KEY\"):\n        print(\"Error: ANTHROPIC_API_KEY environment variable not set\")\n        print(\"\\nGet your API key from: https://console.anthropic.com/\")\n        print(\"\\nThen set it:\")\n        print(\"  export ANTHROPIC_API_KEY='your-api-key-here'\")\n        return\n\n    # Automatically place projects in generations/ directory unless already specified\n    project_dir = args.project_dir\n    if not str(project_dir).startswith(\"generations/\"):\n        # Convert relative paths to be under generations/\n        if project_dir.is_absolute():\n            # If absolute path, use as-is\n            pass\n        else:\n            # Prepend generations/ to relative paths\n            project_dir = Path(\"generations\") / project_dir\n\n    # Run the agent\n    try:\n        asyncio.run(\n            run_autonomous_agent(\n                project_dir=project_dir,\n                model=args.model,\n                max_iterations=args.max_iterations,\n            )\n        )\n    except KeyboardInterrupt:\n        print(\"\\n\\nInterrupted by user\")\n        print(\"To resume, run the same command again\")\n    except Exception as e:\n        print(f\"\\nFatal error: {e}\")\n        raise\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "autonomous-coding/client.py",
    "content": "\"\"\"\nClaude SDK Client Configuration\n===============================\n\nFunctions for creating and configuring the Claude Agent SDK client.\n\"\"\"\n\nimport json\nimport os\nfrom pathlib import Path\n\nfrom claude_code_sdk import ClaudeCodeOptions, ClaudeSDKClient\nfrom claude_code_sdk.types import HookMatcher\n\nfrom security import bash_security_hook\n\n\n# Puppeteer MCP tools for browser automation\nPUPPETEER_TOOLS = [\n    \"mcp__puppeteer__puppeteer_navigate\",\n    \"mcp__puppeteer__puppeteer_screenshot\",\n    \"mcp__puppeteer__puppeteer_click\",\n    \"mcp__puppeteer__puppeteer_fill\",\n    \"mcp__puppeteer__puppeteer_select\",\n    \"mcp__puppeteer__puppeteer_hover\",\n    \"mcp__puppeteer__puppeteer_evaluate\",\n]\n\n# Built-in tools\nBUILTIN_TOOLS = [\n    \"Read\",\n    \"Write\",\n    \"Edit\",\n    \"Glob\",\n    \"Grep\",\n    \"Bash\",\n]\n\n\ndef create_client(project_dir: Path, model: str) -> ClaudeSDKClient:\n    \"\"\"\n    Create a Claude Agent SDK client with multi-layered security.\n\n    Args:\n        project_dir: Directory for the project\n        model: Claude model to use\n\n    Returns:\n        Configured ClaudeSDKClient\n\n    Security layers (defense in depth):\n    1. Sandbox - OS-level bash command isolation prevents filesystem escape\n    2. Permissions - File operations restricted to project_dir only\n    3. Security hooks - Bash commands validated against an allowlist\n       (see security.py for ALLOWED_COMMANDS)\n    \"\"\"\n    api_key = os.environ.get(\"ANTHROPIC_API_KEY\")\n    if not api_key:\n        raise ValueError(\n            \"ANTHROPIC_API_KEY environment variable not set.\\n\"\n            \"Get your API key from: https://console.anthropic.com/\"\n        )\n\n    # Create comprehensive security settings\n    # Note: Using relative paths (\"./**\") restricts access to project directory\n    # since cwd is set to project_dir\n    security_settings = {\n        \"sandbox\": {\"enabled\": True, \"autoAllowBashIfSandboxed\": True},\n        \"permissions\": {\n            \"defaultMode\": \"acceptEdits\",  # Auto-approve edits within allowed directories\n            \"allow\": [\n                # Allow all file operations within the project directory\n                \"Read(./**)\",\n                \"Write(./**)\",\n                \"Edit(./**)\",\n                \"Glob(./**)\",\n                \"Grep(./**)\",\n                # Bash permission granted here, but actual commands are validated\n                # by the bash_security_hook (see security.py for allowed commands)\n                \"Bash(*)\",\n                # Allow Puppeteer MCP tools for browser automation\n                *PUPPETEER_TOOLS,\n            ],\n        },\n    }\n\n    # Ensure project directory exists before creating settings file\n    project_dir.mkdir(parents=True, exist_ok=True)\n\n    # Write settings to a file in the project directory\n    settings_file = project_dir / \".claude_settings.json\"\n    with open(settings_file, \"w\") as f:\n        json.dump(security_settings, f, indent=2)\n\n    print(f\"Created security settings at {settings_file}\")\n    print(\"   - Sandbox enabled (OS-level bash isolation)\")\n    print(f\"   - Filesystem restricted to: {project_dir.resolve()}\")\n    print(\"   - Bash commands restricted to allowlist (see security.py)\")\n    print(\"   - MCP servers: puppeteer (browser automation)\")\n    print()\n\n    return ClaudeSDKClient(\n        options=ClaudeCodeOptions(\n            model=model,\n            system_prompt=\"You are an expert full-stack developer building a production-quality web application.\",\n            allowed_tools=[\n                *BUILTIN_TOOLS,\n                *PUPPETEER_TOOLS,\n            ],\n            mcp_servers={\n                \"puppeteer\": {\"command\": \"npx\", \"args\": [\"puppeteer-mcp-server\"]}\n            },\n            hooks={\n                \"PreToolUse\": [\n                    HookMatcher(matcher=\"Bash\", hooks=[bash_security_hook]),\n                ],\n            },\n            max_turns=1000,\n            cwd=str(project_dir.resolve()),\n            settings=str(settings_file.resolve()),  # Use absolute path\n        )\n    )\n"
  },
  {
    "path": "autonomous-coding/progress.py",
    "content": "\"\"\"\nProgress Tracking Utilities\n===========================\n\nFunctions for tracking and displaying progress of the autonomous coding agent.\n\"\"\"\n\nimport json\nfrom pathlib import Path\n\n\ndef count_passing_tests(project_dir: Path) -> tuple[int, int]:\n    \"\"\"\n    Count passing and total tests in feature_list.json.\n\n    Args:\n        project_dir: Directory containing feature_list.json\n\n    Returns:\n        (passing_count, total_count)\n    \"\"\"\n    tests_file = project_dir / \"feature_list.json\"\n\n    if not tests_file.exists():\n        return 0, 0\n\n    try:\n        with open(tests_file, \"r\") as f:\n            tests = json.load(f)\n\n        total = len(tests)\n        passing = sum(1 for test in tests if test.get(\"passes\", False))\n\n        return passing, total\n    except (json.JSONDecodeError, IOError):\n        return 0, 0\n\n\ndef print_session_header(session_num: int, is_initializer: bool) -> None:\n    \"\"\"Print a formatted header for the session.\"\"\"\n    session_type = \"INITIALIZER\" if is_initializer else \"CODING AGENT\"\n\n    print(\"\\n\" + \"=\" * 70)\n    print(f\"  SESSION {session_num}: {session_type}\")\n    print(\"=\" * 70)\n    print()\n\n\ndef print_progress_summary(project_dir: Path) -> None:\n    \"\"\"Print a summary of current progress.\"\"\"\n    passing, total = count_passing_tests(project_dir)\n\n    if total > 0:\n        percentage = (passing / total) * 100\n        print(f\"\\nProgress: {passing}/{total} tests passing ({percentage:.1f}%)\")\n    else:\n        print(\"\\nProgress: feature_list.json not yet created\")\n"
  },
  {
    "path": "autonomous-coding/prompts/app_spec.txt",
    "content": "<project_specification>\n  <project_name>Claude.ai Clone - AI Chat Interface</project_name>\n\n  <overview>\n    Build a fully functional clone of claude.ai, Anthropic's conversational AI interface. The application should\n    provide a clean, modern chat interface for interacting with Claude via the API, including features like\n    conversation management, artifact rendering, project organization, multiple model selection, and advanced\n    settings. The UI should closely match claude.ai's design using Tailwind CSS with a focus on excellent\n    user experience and responsive design.\n  </overview>\n\n  <technology_stack>\n    <api_key>\n        You can use an API key located at /tmp/api-key for testing. You will not be allowed to read this file, but you can reference it in code.\n    </api_key>\n    <frontend>\n      <framework>React with Vite</framework>\n      <styling>Tailwind CSS (via CDN)</styling>\n      <state_management>React hooks and context</state_management>\n      <routing>React Router for navigation</routing>\n      <markdown>React Markdown for message rendering</markdown>\n      <code_highlighting>Syntax highlighting for code blocks</code_highlighting>\n      <port>Only launch on port {frontend_port}</port>\n    </frontend>\n    <backend>\n      <runtime>Node.js with Express</runtime>\n      <database>SQLite with better-sqlite3</database>\n      <api_integration>Claude API for chat completions</api_integration>\n      <streaming>Server-Sent Events for streaming responses</streaming>\n    </backend>\n    <communication>\n      <api>RESTful endpoints</api>\n      <streaming>SSE for real-time message streaming</streaming>\n      <claude_api>Integration with Claude API using Anthropic SDK</claude_api>\n    </communication>\n  </technology_stack>\n\n  <prerequisites>\n    <environment_setup>\n      - Repository includes .env with VITE_ANTHROPIC_API_KEY configured\n      - Frontend dependencies pre-installed via pnpm\n      - Backend code goes in /server directory\n      - Install backend dependencies as needed\n    </environment_setup>\n  </prerequisites>\n\n  <core_features>\n    <chat_interface>\n      - Clean, centered chat layout with message bubbles\n      - Streaming message responses with typing indicator\n      - Markdown rendering with proper formatting\n      - Code blocks with syntax highlighting and copy button\n      - LaTeX/math equation rendering\n      - Image upload and display in messages\n      - Multi-turn conversations with context\n      - Message editing and regeneration\n      - Stop generation button during streaming\n      - Input field with auto-resize textarea\n      - Character count and token estimation\n      - Keyboard shortcuts (Enter to send, Shift+Enter for newline)\n    </chat_interface>\n\n    <artifacts>\n      - Artifact detection and rendering in side panel\n      - Code artifact viewer with syntax highlighting\n      - HTML/SVG preview with live rendering\n      - React component preview\n      - Mermaid diagram rendering\n      - Text document artifacts\n      - Artifact editing and re-prompting\n      - Full-screen artifact view\n      - Download artifact content\n      - Artifact versioning and history\n    </artifacts>\n\n    <conversation_management>\n      - Create new conversations\n      - Conversation list in sidebar\n      - Rename conversations\n      - Delete conversations\n      - Search conversations by title/content\n      - Pin important conversations\n      - Archive conversations\n      - Conversation folders/organization\n      - Duplicate conversation\n      - Export conversation (JSON, Markdown, PDF)\n      - Conversation timestamps (created, last updated)\n      - Unread message indicators\n    </conversation_management>\n\n    <projects>\n      - Create projects to group related conversations\n      - Project knowledge base (upload documents)\n      - Project-specific custom instructions\n      - Share projects with team (mock feature)\n      - Project settings and configuration\n      - Move conversations between projects\n      - Project templates\n      - Project analytics (usage stats)\n    </projects>\n\n    <model_selection>\n      - Model selector dropdown with the following models:\n        - Claude Sonnet 4.5 (claude-sonnet-4-5-20250929) - default\n        - Claude Haiku 4.5 (claude-haiku-4-5-20251001)\n        - Claude Opus 4.1 (claude-opus-4-1-20250805)\n      - Model capabilities display\n      - Context window indicator\n      - Model-specific pricing info (display only)\n      - Switch models mid-conversation\n      - Model comparison view\n    </model_selection>\n\n    <custom_instructions>\n      - Global custom instructions\n      - Project-specific custom instructions\n      - Conversation-specific system prompts\n      - Custom instruction templates\n      - Preview how instructions affect responses\n    </custom_instructions>\n\n    <settings_preferences>\n      - Theme selection (Light, Dark, Auto)\n      - Font size adjustment\n      - Message density (compact, comfortable, spacious)\n      - Code theme selection\n      - Language preferences\n      - Accessibility options\n      - Keyboard shortcuts reference\n      - Data export options\n      - Privacy settings\n      - API key management\n    </settings_preferences>\n\n    <advanced_features>\n      - Temperature control slider\n      - Max tokens adjustment\n      - Top-p (nucleus sampling) control\n      - System prompt override\n      - Thinking/reasoning mode toggle\n      - Multi-modal input (text + images)\n      - Voice input (optional, mock UI)\n      - Response suggestions\n      - Related prompts\n      - Conversation branching\n    </advanced_features>\n\n    <collaboration>\n      - Share conversation via link (read-only)\n      - Export conversation formats\n      - Conversation templates\n      - Prompt library\n      - Share artifacts\n      - Team workspaces (mock UI)\n    </collaboration>\n\n    <search_discovery>\n      - Search across all conversations\n      - Filter by project, date, model\n      - Prompt library with categories\n      - Example conversations\n      - Quick actions menu\n      - Command palette (Cmd/Ctrl+K)\n    </search_discovery>\n\n    <usage_tracking>\n      - Token usage display per message\n      - Conversation cost estimation\n      - Daily/monthly usage dashboard\n      - Usage limits and warnings\n      - API quota tracking\n    </usage_tracking>\n\n    <onboarding>\n      - Welcome screen for new users\n      - Feature tour highlights\n      - Example prompts to get started\n      - Quick tips and best practices\n      - Keyboard shortcuts tutorial\n    </onboarding>\n\n    <accessibility>\n      - Full keyboard navigation\n      - Screen reader support\n      - ARIA labels and roles\n      - High contrast mode\n      - Focus management\n      - Reduced motion support\n    </accessibility>\n\n    <responsive_design>\n      - Mobile-first responsive layout\n      - Touch-optimized interface\n      - Collapsible sidebar on mobile\n      - Swipe gestures for navigation\n      - Adaptive artifact display\n      - Progressive Web App (PWA) support\n    </responsive_design>\n  </core_features>\n\n  <database_schema>\n    <tables>\n      <users>\n        - id, email, name, avatar_url\n        - created_at, last_login\n        - preferences (JSON: theme, font_size, etc.)\n        - custom_instructions\n      </users>\n\n      <projects>\n        - id, user_id, name, description, color\n        - custom_instructions, knowledge_base_path\n        - created_at, updated_at\n        - is_archived, is_pinned\n      </projects>\n\n      <conversations>\n        - id, user_id, project_id, title\n        - model, created_at, updated_at, last_message_at\n        - is_archived, is_pinned, is_deleted\n        - settings (JSON: temperature, max_tokens, etc.)\n        - token_count, message_count\n      </conversations>\n\n      <messages>\n        - id, conversation_id, role (user/assistant/system)\n        - content, created_at, edited_at\n        - tokens, finish_reason\n        - images (JSON array of image data)\n        - parent_message_id (for branching)\n      </messages>\n\n      <artifacts>\n        - id, message_id, conversation_id\n        - type (code/html/svg/react/mermaid/text)\n        - title, identifier, language\n        - content, version\n        - created_at, updated_at\n      </artifacts>\n\n      <shared_conversations>\n        - id, conversation_id, share_token\n        - created_at, expires_at, view_count\n        - is_public\n      </shared_conversations>\n\n      <prompt_library>\n        - id, user_id, title, description\n        - prompt_template, category, tags (JSON)\n        - is_public, usage_count\n        - created_at, updated_at\n      </prompt_library>\n\n      <conversation_folders>\n        - id, user_id, project_id, name, parent_folder_id\n        - created_at, position\n      </conversation_folders>\n\n      <conversation_folder_items>\n        - id, folder_id, conversation_id\n      </conversation_folder_items>\n\n      <usage_tracking>\n        - id, user_id, conversation_id, message_id\n        - model, input_tokens, output_tokens\n        - cost_estimate, created_at\n      </usage_tracking>\n\n      <api_keys>\n        - id, user_id, key_name, api_key_hash\n        - created_at, last_used_at\n        - is_active\n      </api_keys>\n    </tables>\n  </database_schema>\n\n  <api_endpoints_summary>\n    <authentication>\n      - POST /api/auth/login\n      - POST /api/auth/logout\n      - GET /api/auth/me\n      - PUT /api/auth/profile\n    </authentication>\n\n    <conversations>\n      - GET /api/conversations\n      - POST /api/conversations\n      - GET /api/conversations/:id\n      - PUT /api/conversations/:id\n      - DELETE /api/conversations/:id\n      - POST /api/conversations/:id/duplicate\n      - POST /api/conversations/:id/export\n      - PUT /api/conversations/:id/archive\n      - PUT /api/conversations/:id/pin\n      - POST /api/conversations/:id/branch\n    </conversations>\n\n    <messages>\n      - GET /api/conversations/:id/messages\n      - POST /api/conversations/:id/messages\n      - PUT /api/messages/:id\n      - DELETE /api/messages/:id\n      - POST /api/messages/:id/regenerate\n      - GET /api/messages/stream (SSE endpoint)\n    </messages>\n\n    <artifacts>\n      - GET /api/conversations/:id/artifacts\n      - GET /api/artifacts/:id\n      - PUT /api/artifacts/:id\n      - DELETE /api/artifacts/:id\n      - POST /api/artifacts/:id/fork\n      - GET /api/artifacts/:id/versions\n    </artifacts>\n\n    <projects>\n      - GET /api/projects\n      - POST /api/projects\n      - GET /api/projects/:id\n      - PUT /api/projects/:id\n      - DELETE /api/projects/:id\n      - POST /api/projects/:id/knowledge\n      - GET /api/projects/:id/conversations\n      - PUT /api/projects/:id/settings\n    </projects>\n\n    <sharing>\n      - POST /api/conversations/:id/share\n      - GET /api/share/:token\n      - DELETE /api/share/:token\n      - PUT /api/share/:token/settings\n    </sharing>\n\n    <prompts>\n      - GET /api/prompts/library\n      - POST /api/prompts/library\n      - GET /api/prompts/:id\n      - PUT /api/prompts/:id\n      - DELETE /api/prompts/:id\n      - GET /api/prompts/categories\n      - GET /api/prompts/examples\n    </prompts>\n\n    <search>\n      - GET /api/search/conversations?q=query\n      - GET /api/search/messages?q=query\n      - GET /api/search/artifacts?q=query\n      - GET /api/search/prompts?q=query\n    </search>\n\n    <folders>\n      - GET /api/folders\n      - POST /api/folders\n      - PUT /api/folders/:id\n      - DELETE /api/folders/:id\n      - POST /api/folders/:id/items\n      - DELETE /api/folders/:id/items/:conversationId\n    </folders>\n\n    <usage>\n      - GET /api/usage/daily\n      - GET /api/usage/monthly\n      - GET /api/usage/by-model\n      - GET /api/usage/conversations/:id\n    </usage>\n\n    <settings>\n      - GET /api/settings\n      - PUT /api/settings\n      - GET /api/settings/custom-instructions\n      - PUT /api/settings/custom-instructions\n    </settings>\n\n    <claude_api>\n      - POST /api/claude/chat (proxy to Claude API)\n      - POST /api/claude/chat/stream (streaming proxy)\n      - GET /api/claude/models\n      - POST /api/claude/images/upload\n    </claude_api>\n  </api_endpoints_summary>\n\n  <ui_layout>\n    <main_structure>\n      - Three-column layout: sidebar (conversations), main (chat), panel (artifacts)\n      - Collapsible sidebar with resize handle\n      - Responsive breakpoints: mobile (single column), tablet (two column), desktop (three column)\n      - Persistent header with project/model selector\n      - Bottom input area with send button and options\n    </main_structure>\n\n    <sidebar_left>\n      - New chat button (prominent)\n      - Project selector dropdown\n      - Search conversations input\n      - Conversations list (grouped by date: Today, Yesterday, Previous 7 days, etc.)\n      - Folder tree view (collapsible)\n      - Settings gear icon at bottom\n      - User profile at bottom\n    </sidebar_left>\n\n    <main_chat_area>\n      - Conversation title (editable inline)\n      - Model selector badge\n      - Message history (scrollable)\n      - Welcome screen for new conversations\n      - Suggested prompts (empty state)\n      - Input area with formatting toolbar\n      - Attachment button for images\n      - Send button with loading state\n      - Stop generation button\n    </main_chat_area>\n\n    <artifacts_panel>\n      - Artifact header with title and type badge\n      - Code editor or preview pane\n      - Tabs for multiple artifacts\n      - Full-screen toggle\n      - Download button\n      - Edit/Re-prompt button\n      - Version selector\n      - Close panel button\n    </artifacts_panel>\n\n    <modals_overlays>\n      - Settings modal (tabbed interface)\n      - Share conversation modal\n      - Export options modal\n      - Project settings modal\n      - Prompt library modal\n      - Command palette overlay\n      - Keyboard shortcuts reference\n    </modals_overlays>\n  </ui_layout>\n\n  <design_system>\n    <color_palette>\n      - Primary: Orange/amber accent (#CC785C claude-style)\n      - Background: White (light mode), Dark gray (#1A1A1A dark mode)\n      - Surface: Light gray (#F5F5F5 light), Darker gray (#2A2A2A dark)\n      - Text: Near black (#1A1A1A light), Off-white (#E5E5E5 dark)\n      - Borders: Light gray (#E5E5E5 light), Dark gray (#404040 dark)\n      - Code blocks: Monaco editor theme\n    </color_palette>\n\n    <typography>\n      - Sans-serif system font stack (Inter, SF Pro, Roboto, system-ui)\n      - Headings: font-semibold\n      - Body: font-normal, leading-relaxed\n      - Code: Monospace (JetBrains Mono, Consolas, Monaco)\n      - Message text: text-base (16px), comfortable line-height\n    </typography>\n\n    <components>\n      <message_bubble>\n        - User messages: Right-aligned, subtle background\n        - Assistant messages: Left-aligned, no background\n        - Markdown formatting with proper spacing\n        - Inline code with bg-gray-100 background\n        - Code blocks with syntax highlighting\n        - Copy button on code blocks\n      </message_bubble>\n\n      <buttons>\n        - Primary: Orange/amber background, white text, rounded\n        - Secondary: Border style with hover fill\n        - Icon buttons: Square with hover background\n        - Disabled state: Reduced opacity, no pointer events\n      </buttons>\n\n      <inputs>\n        - Rounded borders with focus ring\n        - Textarea auto-resize\n        - Placeholder text in gray\n        - Error states in red\n        - Character counter\n      </inputs>\n\n      <cards>\n        - Subtle border or shadow\n        - Rounded corners (8px)\n        - Padding: p-4 to p-6\n        - Hover state: slight shadow increase\n      </cards>\n    </components>\n\n    <animations>\n      - Smooth transitions (150-300ms)\n      - Fade in for new messages\n      - Slide in for sidebar\n      - Typing indicator animation\n      - Loading spinner for generation\n      - Skeleton loaders for content\n    </animations>\n  </design_system>\n\n  <key_interactions>\n    <message_flow>\n      1. User types message in input field\n      2. Optional: Attach images via button\n      3. Click send or press Enter\n      4. Message appears in chat immediately\n      5. Typing indicator shows while waiting\n      6. Response streams in word by word\n      7. Code blocks render with syntax highlighting\n      8. Artifacts detected and rendered in side panel\n      9. Message complete, enable regenerate option\n    </message_flow>\n\n    <artifact_flow>\n      1. Assistant generates artifact in response\n      2. Artifact panel slides in from right\n      3. Content renders (code with highlighting or live preview)\n      4. User can edit artifact inline\n      5. \"Re-prompt\" button to iterate with Claude\n      6. Download or copy artifact content\n      7. Full-screen mode for detailed work\n      8. Close panel to return to chat focus\n    </artifact_flow>\n\n    <conversation_management>\n      1. Click \"New Chat\" to start fresh conversation\n      2. Conversations auto-save with first message\n      3. Auto-generate title from first exchange\n      4. Click title to rename inline\n      5. Drag conversations into folders\n      6. Right-click for context menu (pin, archive, delete, export)\n      7. Search filters conversations in real-time\n      8. Click conversation to switch context\n    </conversation_management>\n  </key_interactions>\n\n  <implementation_steps>\n    <step number=\"1\">\n      <title>Setup Project Foundation and Database</title>\n      <tasks>\n        - Initialize Express server with SQLite database\n        - Set up Claude API client with streaming support\n        - Create database schema with migrations\n        - Implement authentication endpoints\n        - Set up basic CORS and middleware\n        - Create health check endpoint\n      </tasks>\n    </step>\n\n    <step number=\"2\">\n      <title>Build Core Chat Interface</title>\n      <tasks>\n        - Create main layout with sidebar and chat area\n        - Implement message display with markdown rendering\n        - Add streaming message support with SSE\n        - Build input area with auto-resize textarea\n        - Add code block syntax highlighting\n        - Implement stop generation functionality\n        - Add typing indicators and loading states\n      </tasks>\n    </step>\n\n    <step number=\"3\">\n      <title>Conversation Management</title>\n      <tasks>\n        - Create conversation list in sidebar\n        - Implement new conversation creation\n        - Add conversation switching\n        - Build conversation rename functionality\n        - Implement delete with confirmation\n        - Add conversation search\n        - Create conversation grouping by date\n      </tasks>\n    </step>\n\n    <step number=\"4\">\n      <title>Artifacts System</title>\n      <tasks>\n        - Build artifact detection from Claude responses\n        - Create artifact rendering panel\n        - Implement code artifact viewer\n        - Add HTML/SVG live preview\n        - Build artifact editing interface\n        - Add artifact versioning\n        - Implement full-screen artifact view\n      </tasks>\n    </step>\n\n    <step number=\"5\">\n      <title>Projects and Organization</title>\n      <tasks>\n        - Create projects CRUD endpoints\n        - Build project selector UI\n        - Implement project-specific custom instructions\n        - Add folder system for conversations\n        - Create drag-and-drop organization\n        - Build project settings panel\n      </tasks>\n    </step>\n\n    <step number=\"6\">\n      <title>Advanced Features</title>\n      <tasks>\n        - Add model selection dropdown\n        - Implement temperature and parameter controls\n        - Build image upload functionality\n        - Create message editing and regeneration\n        - Add conversation branching\n        - Implement export functionality\n      </tasks>\n    </step>\n\n    <step number=\"7\">\n      <title>Settings and Customization</title>\n      <tasks>\n        - Build settings modal with tabs\n        - Implement theme switching (light/dark)\n        - Add custom instructions management\n        - Create keyboard shortcuts\n        - Build prompt library\n        - Add usage tracking dashboard\n      </tasks>\n    </step>\n\n    <step number=\"8\">\n      <title>Sharing and Collaboration</title>\n      <tasks>\n        - Implement conversation sharing with tokens\n        - Create public share view\n        - Add export to multiple formats\n        - Build prompt templates\n        - Create example conversations\n      </tasks>\n    </step>\n\n    <step number=\"9\">\n      <title>Polish and Optimization</title>\n      <tasks>\n        - Optimize for mobile responsiveness\n        - Add command palette (Cmd+K)\n        - Implement comprehensive keyboard navigation\n        - Add onboarding flow\n        - Create accessibility improvements\n        - Performance optimization and caching\n      </tasks>\n    </step>\n  </implementation_steps>\n\n  <success_criteria>\n    <functionality>\n      - Streaming chat responses work smoothly\n      - Artifact detection and rendering accurate\n      - Conversation management intuitive and reliable\n      - Project organization clear and useful\n      - Image upload and display working\n      - All CRUD operations functional\n    </functionality>\n\n    <user_experience>\n      - Interface matches claude.ai design language\n      - Responsive on all device sizes\n      - Smooth animations and transitions\n      - Fast response times and minimal lag\n      - Intuitive navigation and workflows\n      - Clear feedback for all actions\n    </user_experience>\n\n    <technical_quality>\n      - Clean, maintainable code structure\n      - Proper error handling throughout\n      - Secure API key management\n      - Optimized database queries\n      - Efficient streaming implementation\n      - Comprehensive testing coverage\n    </technical_quality>\n\n    <design_polish>\n      - Consistent with claude.ai visual design\n      - Beautiful typography and spacing\n      - Smooth animations and micro-interactions\n      - Excellent contrast and accessibility\n      - Professional, polished appearance\n      - Dark mode fully implemented\n    </design_polish>\n  </success_criteria>\n</project_specification>\n"
  },
  {
    "path": "autonomous-coding/prompts/coding_prompt.md",
    "content": "## YOUR ROLE - CODING AGENT\n\nYou are continuing work on a long-running autonomous development task.\nThis is a FRESH context window - you have no memory of previous sessions.\n\n### STEP 1: GET YOUR BEARINGS (MANDATORY)\n\nStart by orienting yourself:\n\n```bash\n# 1. See your working directory\npwd\n\n# 2. List files to understand project structure\nls -la\n\n# 3. Read the project specification to understand what you're building\ncat app_spec.txt\n\n# 4. Read the feature list to see all work\ncat feature_list.json | head -50\n\n# 5. Read progress notes from previous sessions\ncat claude-progress.txt\n\n# 6. Check recent git history\ngit log --oneline -20\n\n# 7. Count remaining tests\ncat feature_list.json | grep '\"passes\": false' | wc -l\n```\n\nUnderstanding the `app_spec.txt` is critical - it contains the full requirements\nfor the application you're building.\n\n### STEP 2: START SERVERS (IF NOT RUNNING)\n\nIf `init.sh` exists, run it:\n```bash\nchmod +x init.sh\n./init.sh\n```\n\nOtherwise, start servers manually and document the process.\n\n### STEP 3: VERIFICATION TEST (CRITICAL!)\n\n**MANDATORY BEFORE NEW WORK:**\n\nThe previous session may have introduced bugs. Before implementing anything\nnew, you MUST run verification tests.\n\nRun 1-2 of the feature tests marked as `\"passes\": true` that are most core to the app's functionality to verify they still work.\nFor example, if this were a chat app, you should perform a test that logs into the app, sends a message, and gets a response.\n\n**If you find ANY issues (functional or visual):**\n- Mark that feature as \"passes\": false immediately\n- Add issues to a list\n- Fix all issues BEFORE moving to new features\n- This includes UI bugs like:\n  * White-on-white text or poor contrast\n  * Random characters displayed\n  * Incorrect timestamps\n  * Layout issues or overflow\n  * Buttons too close together\n  * Missing hover states\n  * Console errors\n\n### STEP 4: CHOOSE ONE FEATURE TO IMPLEMENT\n\nLook at feature_list.json and find the highest-priority feature with \"passes\": false.\n\nFocus on completing one feature perfectly and completing its testing steps in this session before moving on to other features.\nIt's ok if you only complete one feature in this session, as there will be more sessions later that continue to make progress.\n\n### STEP 5: IMPLEMENT THE FEATURE\n\nImplement the chosen feature thoroughly:\n1. Write the code (frontend and/or backend as needed)\n2. Test manually using browser automation (see Step 6)\n3. Fix any issues discovered\n4. Verify the feature works end-to-end\n\n### STEP 6: VERIFY WITH BROWSER AUTOMATION\n\n**CRITICAL:** You MUST verify features through the actual UI.\n\nUse browser automation tools:\n- Navigate to the app in a real browser\n- Interact like a human user (click, type, scroll)\n- Take screenshots at each step\n- Verify both functionality AND visual appearance\n\n**DO:**\n- Test through the UI with clicks and keyboard input\n- Take screenshots to verify visual appearance\n- Check for console errors in browser\n- Verify complete user workflows end-to-end\n\n**DON'T:**\n- Only test with curl commands (backend testing alone is insufficient)\n- Use JavaScript evaluation to bypass UI (no shortcuts)\n- Skip visual verification\n- Mark tests passing without thorough verification\n\n### STEP 7: UPDATE feature_list.json (CAREFULLY!)\n\n**YOU CAN ONLY MODIFY ONE FIELD: \"passes\"**\n\nAfter thorough verification, change:\n```json\n\"passes\": false\n```\nto:\n```json\n\"passes\": true\n```\n\n**NEVER:**\n- Remove tests\n- Edit test descriptions\n- Modify test steps\n- Combine or consolidate tests\n- Reorder tests\n\n**ONLY CHANGE \"passes\" FIELD AFTER VERIFICATION WITH SCREENSHOTS.**\n\n### STEP 8: COMMIT YOUR PROGRESS\n\nMake a descriptive git commit:\n```bash\ngit add .\ngit commit -m \"Implement [feature name] - verified end-to-end\n\n- Added [specific changes]\n- Tested with browser automation\n- Updated feature_list.json: marked test #X as passing\n- Screenshots in verification/ directory\n\"\n```\n\n### STEP 9: UPDATE PROGRESS NOTES\n\nUpdate `claude-progress.txt` with:\n- What you accomplished this session\n- Which test(s) you completed\n- Any issues discovered or fixed\n- What should be worked on next\n- Current completion status (e.g., \"45/200 tests passing\")\n\n### STEP 10: END SESSION CLEANLY\n\nBefore context fills up:\n1. Commit all working code\n2. Update claude-progress.txt\n3. Update feature_list.json if tests verified\n4. Ensure no uncommitted changes\n5. Leave app in working state (no broken features)\n\n---\n\n## TESTING REQUIREMENTS\n\n**ALL testing must use browser automation tools.**\n\nAvailable tools:\n- puppeteer_navigate - Start browser and go to URL\n- puppeteer_screenshot - Capture screenshot\n- puppeteer_click - Click elements\n- puppeteer_fill - Fill form inputs\n- puppeteer_evaluate - Execute JavaScript (use sparingly, only for debugging)\n\nTest like a human user with mouse and keyboard. Don't take shortcuts by using JavaScript evaluation.\nDon't use the puppeteer \"active tab\" tool.\n\n---\n\n## IMPORTANT REMINDERS\n\n**Your Goal:** Production-quality application with all 200+ tests passing\n\n**This Session's Goal:** Complete at least one feature perfectly\n\n**Priority:** Fix broken tests before implementing new features\n\n**Quality Bar:**\n- Zero console errors\n- Polished UI matching the design specified in app_spec.txt\n- All features work end-to-end through the UI\n- Fast, responsive, professional\n\n**You have unlimited time.** Take as long as needed to get it right. The most important thing is that you\nleave the code base in a clean state before terminating the session (Step 10).\n\n---\n\nBegin by running Step 1 (Get Your Bearings).\n"
  },
  {
    "path": "autonomous-coding/prompts/initializer_prompt.md",
    "content": "## YOUR ROLE - INITIALIZER AGENT (Session 1 of Many)\n\nYou are the FIRST agent in a long-running autonomous development process.\nYour job is to set up the foundation for all future coding agents.\n\n### FIRST: Read the Project Specification\n\nStart by reading `app_spec.txt` in your working directory. This file contains\nthe complete specification for what you need to build. Read it carefully\nbefore proceeding.\n\n### CRITICAL FIRST TASK: Create feature_list.json\n\nBased on `app_spec.txt`, create a file called `feature_list.json` with 200 detailed\nend-to-end test cases. This file is the single source of truth for what\nneeds to be built.\n\n**Format:**\n```json\n[\n  {\n    \"category\": \"functional\",\n    \"description\": \"Brief description of the feature and what this test verifies\",\n    \"steps\": [\n      \"Step 1: Navigate to relevant page\",\n      \"Step 2: Perform action\",\n      \"Step 3: Verify expected result\"\n    ],\n    \"passes\": false\n  },\n  {\n    \"category\": \"style\",\n    \"description\": \"Brief description of UI/UX requirement\",\n    \"steps\": [\n      \"Step 1: Navigate to page\",\n      \"Step 2: Take screenshot\",\n      \"Step 3: Verify visual requirements\"\n    ],\n    \"passes\": false\n  }\n]\n```\n\n**Requirements for feature_list.json:**\n- Minimum 200 features total with testing steps for each\n- Both \"functional\" and \"style\" categories\n- Mix of narrow tests (2-5 steps) and comprehensive tests (10+ steps)\n- At least 25 tests MUST have 10+ steps each\n- Order features by priority: fundamental features first\n- ALL tests start with \"passes\": false\n- Cover every feature in the spec exhaustively\n\n**CRITICAL INSTRUCTION:**\nIT IS CATASTROPHIC TO REMOVE OR EDIT FEATURES IN FUTURE SESSIONS.\nFeatures can ONLY be marked as passing (change \"passes\": false to \"passes\": true).\nNever remove features, never edit descriptions, never modify testing steps.\nThis ensures no functionality is missed.\n\n### SECOND TASK: Create init.sh\n\nCreate a script called `init.sh` that future agents can use to quickly\nset up and run the development environment. The script should:\n\n1. Install any required dependencies\n2. Start any necessary servers or services\n3. Print helpful information about how to access the running application\n\nBase the script on the technology stack specified in `app_spec.txt`.\n\n### THIRD TASK: Initialize Git\n\nCreate a git repository and make your first commit with:\n- feature_list.json (complete with all 200+ features)\n- init.sh (environment setup script)\n- README.md (project overview and setup instructions)\n\nCommit message: \"Initial setup: feature_list.json, init.sh, and project structure\"\n\n### FOURTH TASK: Create Project Structure\n\nSet up the basic project structure based on what's specified in `app_spec.txt`.\nThis typically includes directories for frontend, backend, and any other\ncomponents mentioned in the spec.\n\n### OPTIONAL: Start Implementation\n\nIf you have time remaining in this session, you may begin implementing\nthe highest-priority features from feature_list.json. Remember:\n- Work on ONE feature at a time\n- Test thoroughly before marking \"passes\": true\n- Commit your progress before session ends\n\n### ENDING THIS SESSION\n\nBefore your context fills up:\n1. Commit all work with descriptive messages\n2. Create `claude-progress.txt` with a summary of what you accomplished\n3. Ensure feature_list.json is complete and saved\n4. Leave the environment in a clean, working state\n\nThe next agent will continue from here with a fresh context window.\n\n---\n\n**Remember:** You have unlimited time across many sessions. Focus on\nquality over speed. Production-ready is the goal.\n"
  },
  {
    "path": "autonomous-coding/prompts.py",
    "content": "\"\"\"\nPrompt Loading Utilities\n========================\n\nFunctions for loading prompt templates from the prompts directory.\n\"\"\"\n\nimport shutil\nfrom pathlib import Path\n\n\nPROMPTS_DIR = Path(__file__).parent / \"prompts\"\n\n\ndef load_prompt(name: str) -> str:\n    \"\"\"Load a prompt template from the prompts directory.\"\"\"\n    prompt_path = PROMPTS_DIR / f\"{name}.md\"\n    return prompt_path.read_text()\n\n\ndef get_initializer_prompt() -> str:\n    \"\"\"Load the initializer prompt.\"\"\"\n    return load_prompt(\"initializer_prompt\")\n\n\ndef get_coding_prompt() -> str:\n    \"\"\"Load the coding agent prompt.\"\"\"\n    return load_prompt(\"coding_prompt\")\n\n\ndef copy_spec_to_project(project_dir: Path) -> None:\n    \"\"\"Copy the app spec file into the project directory for the agent to read.\"\"\"\n    spec_source = PROMPTS_DIR / \"app_spec.txt\"\n    spec_dest = project_dir / \"app_spec.txt\"\n    if not spec_dest.exists():\n        shutil.copy(spec_source, spec_dest)\n        print(\"Copied app_spec.txt to project directory\")\n"
  },
  {
    "path": "autonomous-coding/requirements.txt",
    "content": "claude-code-sdk>=0.0.25\n"
  },
  {
    "path": "autonomous-coding/security.py",
    "content": "\"\"\"\nSecurity Hooks for Autonomous Coding Agent\n==========================================\n\nPre-tool-use hooks that validate bash commands for security.\nUses an allowlist approach - only explicitly permitted commands can run.\n\"\"\"\n\nimport os\nimport shlex\n\n\n# Allowed commands for development tasks\n# Minimal set needed for the autonomous coding demo\nALLOWED_COMMANDS = {\n    # File inspection\n    \"ls\",\n    \"cat\",\n    \"head\",\n    \"tail\",\n    \"wc\",\n    \"grep\",\n    # File operations (agent uses SDK tools for most file ops, but cp/mkdir needed occasionally)\n    \"cp\",\n    \"mkdir\",\n    \"chmod\",  # For making scripts executable; validated separately\n    # Directory\n    \"pwd\",\n    # Node.js development\n    \"npm\",\n    \"node\",\n    # Version control\n    \"git\",\n    # Process management\n    \"ps\",\n    \"lsof\",\n    \"sleep\",\n    \"pkill\",  # For killing dev servers; validated separately\n    # Script execution\n    \"init.sh\",  # Init scripts; validated separately\n}\n\n# Commands that need additional validation even when in the allowlist\nCOMMANDS_NEEDING_EXTRA_VALIDATION = {\"pkill\", \"chmod\", \"init.sh\"}\n\n\ndef split_command_segments(command_string: str) -> list[str]:\n    \"\"\"\n    Split a compound command into individual command segments.\n\n    Handles command chaining (&&, ||, ;) but not pipes (those are single commands).\n\n    Args:\n        command_string: The full shell command\n\n    Returns:\n        List of individual command segments\n    \"\"\"\n    import re\n\n    # Split on && and || while preserving the ability to handle each segment\n    # This regex splits on && or || that aren't inside quotes\n    segments = re.split(r\"\\s*(?:&&|\\|\\|)\\s*\", command_string)\n\n    # Further split on semicolons\n    result = []\n    for segment in segments:\n        sub_segments = re.split(r'(?<![\"\\'])\\s*;\\s*(?![\"\\'])', segment)\n        for sub in sub_segments:\n            sub = sub.strip()\n            if sub:\n                result.append(sub)\n\n    return result\n\n\ndef extract_commands(command_string: str) -> list[str]:\n    \"\"\"\n    Extract command names from a shell command string.\n\n    Handles pipes, command chaining (&&, ||, ;), and subshells.\n    Returns the base command names (without paths).\n\n    Args:\n        command_string: The full shell command\n\n    Returns:\n        List of command names found in the string\n    \"\"\"\n    commands = []\n\n    # shlex doesn't treat ; as a separator, so we need to pre-process\n    import re\n\n    # Split on semicolons that aren't inside quotes (simple heuristic)\n    # This handles common cases like \"echo hello; ls\"\n    segments = re.split(r'(?<![\"\\'])\\s*;\\s*(?![\"\\'])', command_string)\n\n    for segment in segments:\n        segment = segment.strip()\n        if not segment:\n            continue\n\n        try:\n            tokens = shlex.split(segment)\n        except ValueError:\n            # Malformed command (unclosed quotes, etc.)\n            # Return empty to trigger block (fail-safe)\n            return []\n\n        if not tokens:\n            continue\n\n        # Track when we expect a command vs arguments\n        expect_command = True\n\n        for token in tokens:\n            # Shell operators indicate a new command follows\n            if token in (\"|\", \"||\", \"&&\", \"&\"):\n                expect_command = True\n                continue\n\n            # Skip shell keywords that precede commands\n            if token in (\n                \"if\",\n                \"then\",\n                \"else\",\n                \"elif\",\n                \"fi\",\n                \"for\",\n                \"while\",\n                \"until\",\n                \"do\",\n                \"done\",\n                \"case\",\n                \"esac\",\n                \"in\",\n                \"!\",\n                \"{\",\n                \"}\",\n            ):\n                continue\n\n            # Skip flags/options\n            if token.startswith(\"-\"):\n                continue\n\n            # Skip variable assignments (VAR=value)\n            if \"=\" in token and not token.startswith(\"=\"):\n                continue\n\n            if expect_command:\n                # Extract the base command name (handle paths like /usr/bin/python)\n                cmd = os.path.basename(token)\n                commands.append(cmd)\n                expect_command = False\n\n    return commands\n\n\ndef validate_pkill_command(command_string: str) -> tuple[bool, str]:\n    \"\"\"\n    Validate pkill commands - only allow killing dev-related processes.\n\n    Uses shlex to parse the command, avoiding regex bypass vulnerabilities.\n\n    Returns:\n        Tuple of (is_allowed, reason_if_blocked)\n    \"\"\"\n    # Allowed process names for pkill\n    allowed_process_names = {\n        \"node\",\n        \"npm\",\n        \"npx\",\n        \"vite\",\n        \"next\",\n    }\n\n    try:\n        tokens = shlex.split(command_string)\n    except ValueError:\n        return False, \"Could not parse pkill command\"\n\n    if not tokens:\n        return False, \"Empty pkill command\"\n\n    # Separate flags from arguments\n    args = []\n    for token in tokens[1:]:\n        if not token.startswith(\"-\"):\n            args.append(token)\n\n    if not args:\n        return False, \"pkill requires a process name\"\n\n    # The target is typically the last non-flag argument\n    target = args[-1]\n\n    # For -f flag (full command line match), extract the first word as process name\n    # e.g., \"pkill -f 'node server.js'\" -> target is \"node server.js\", process is \"node\"\n    if \" \" in target:\n        target = target.split()[0]\n\n    if target in allowed_process_names:\n        return True, \"\"\n    return False, f\"pkill only allowed for dev processes: {allowed_process_names}\"\n\n\ndef validate_chmod_command(command_string: str) -> tuple[bool, str]:\n    \"\"\"\n    Validate chmod commands - only allow making files executable with +x.\n\n    Returns:\n        Tuple of (is_allowed, reason_if_blocked)\n    \"\"\"\n    try:\n        tokens = shlex.split(command_string)\n    except ValueError:\n        return False, \"Could not parse chmod command\"\n\n    if not tokens or tokens[0] != \"chmod\":\n        return False, \"Not a chmod command\"\n\n    # Look for the mode argument\n    # Valid modes: +x, u+x, a+x, etc. (anything ending with +x for execute permission)\n    mode = None\n    files = []\n\n    for token in tokens[1:]:\n        if token.startswith(\"-\"):\n            # Skip flags like -R (we don't allow recursive chmod anyway)\n            return False, \"chmod flags are not allowed\"\n        elif mode is None:\n            mode = token\n        else:\n            files.append(token)\n\n    if mode is None:\n        return False, \"chmod requires a mode\"\n\n    if not files:\n        return False, \"chmod requires at least one file\"\n\n    # Only allow +x variants (making files executable)\n    # This matches: +x, u+x, g+x, o+x, a+x, ug+x, etc.\n    import re\n\n    if not re.match(r\"^[ugoa]*\\+x$\", mode):\n        return False, f\"chmod only allowed with +x mode, got: {mode}\"\n\n    return True, \"\"\n\n\ndef validate_init_script(command_string: str) -> tuple[bool, str]:\n    \"\"\"\n    Validate init.sh script execution - only allow ./init.sh.\n\n    Returns:\n        Tuple of (is_allowed, reason_if_blocked)\n    \"\"\"\n    try:\n        tokens = shlex.split(command_string)\n    except ValueError:\n        return False, \"Could not parse init script command\"\n\n    if not tokens:\n        return False, \"Empty command\"\n\n    # The command should be exactly ./init.sh (possibly with arguments)\n    script = tokens[0]\n\n    # Allow ./init.sh or paths ending in /init.sh\n    if script == \"./init.sh\" or script.endswith(\"/init.sh\"):\n        return True, \"\"\n\n    return False, f\"Only ./init.sh is allowed, got: {script}\"\n\n\ndef get_command_for_validation(cmd: str, segments: list[str]) -> str:\n    \"\"\"\n    Find the specific command segment that contains the given command.\n\n    Args:\n        cmd: The command name to find\n        segments: List of command segments\n\n    Returns:\n        The segment containing the command, or empty string if not found\n    \"\"\"\n    for segment in segments:\n        segment_commands = extract_commands(segment)\n        if cmd in segment_commands:\n            return segment\n    return \"\"\n\n\nasync def bash_security_hook(input_data, tool_use_id=None, context=None):\n    \"\"\"\n    Pre-tool-use hook that validates bash commands using an allowlist.\n\n    Only commands in ALLOWED_COMMANDS are permitted.\n\n    Args:\n        input_data: Dict containing tool_name and tool_input\n        tool_use_id: Optional tool use ID\n        context: Optional context\n\n    Returns:\n        Empty dict to allow, or {\"decision\": \"block\", \"reason\": \"...\"} to block\n    \"\"\"\n    if input_data.get(\"tool_name\") != \"Bash\":\n        return {}\n\n    command = input_data.get(\"tool_input\", {}).get(\"command\", \"\")\n    if not command:\n        return {}\n\n    # Extract all commands from the command string\n    commands = extract_commands(command)\n\n    if not commands:\n        # Could not parse - fail safe by blocking\n        return {\n            \"decision\": \"block\",\n            \"reason\": f\"Could not parse command for security validation: {command}\",\n        }\n\n    # Split into segments for per-command validation\n    segments = split_command_segments(command)\n\n    # Check each command against the allowlist\n    for cmd in commands:\n        if cmd not in ALLOWED_COMMANDS:\n            return {\n                \"decision\": \"block\",\n                \"reason\": f\"Command '{cmd}' is not in the allowed commands list\",\n            }\n\n        # Additional validation for sensitive commands\n        if cmd in COMMANDS_NEEDING_EXTRA_VALIDATION:\n            # Find the specific segment containing this command\n            cmd_segment = get_command_for_validation(cmd, segments)\n            if not cmd_segment:\n                cmd_segment = command  # Fallback to full command\n\n            if cmd == \"pkill\":\n                allowed, reason = validate_pkill_command(cmd_segment)\n                if not allowed:\n                    return {\"decision\": \"block\", \"reason\": reason}\n            elif cmd == \"chmod\":\n                allowed, reason = validate_chmod_command(cmd_segment)\n                if not allowed:\n                    return {\"decision\": \"block\", \"reason\": reason}\n            elif cmd == \"init.sh\":\n                allowed, reason = validate_init_script(cmd_segment)\n                if not allowed:\n                    return {\"decision\": \"block\", \"reason\": reason}\n\n    return {}\n"
  },
  {
    "path": "autonomous-coding/test_security.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nSecurity Hook Tests\n===================\n\nTests for the bash command security validation logic.\nRun with: python test_security.py\n\"\"\"\n\nimport asyncio\nimport sys\n\nfrom security import (\n    bash_security_hook,\n    extract_commands,\n    validate_chmod_command,\n    validate_init_script,\n)\n\n\ndef test_hook(command: str, should_block: bool) -> bool:\n    \"\"\"Test a single command against the security hook.\"\"\"\n    input_data = {\"tool_name\": \"Bash\", \"tool_input\": {\"command\": command}}\n    result = asyncio.run(bash_security_hook(input_data))\n    was_blocked = result.get(\"decision\") == \"block\"\n\n    if was_blocked == should_block:\n        status = \"PASS\"\n    else:\n        status = \"FAIL\"\n        expected = \"blocked\" if should_block else \"allowed\"\n        actual = \"blocked\" if was_blocked else \"allowed\"\n        reason = result.get(\"reason\", \"\")\n        print(f\"  {status}: {command!r}\")\n        print(f\"         Expected: {expected}, Got: {actual}\")\n        if reason:\n            print(f\"         Reason: {reason}\")\n        return False\n\n    print(f\"  {status}: {command!r}\")\n    return True\n\n\ndef test_extract_commands():\n    \"\"\"Test the command extraction logic.\"\"\"\n    print(\"\\nTesting command extraction:\\n\")\n    passed = 0\n    failed = 0\n\n    test_cases = [\n        (\"ls -la\", [\"ls\"]),\n        (\"npm install && npm run build\", [\"npm\", \"npm\"]),\n        (\"cat file.txt | grep pattern\", [\"cat\", \"grep\"]),\n        (\"/usr/bin/node script.js\", [\"node\"]),\n        (\"VAR=value ls\", [\"ls\"]),\n        (\"git status || git init\", [\"git\", \"git\"]),\n    ]\n\n    for cmd, expected in test_cases:\n        result = extract_commands(cmd)\n        if result == expected:\n            print(f\"  PASS: {cmd!r} -> {result}\")\n            passed += 1\n        else:\n            print(f\"  FAIL: {cmd!r}\")\n            print(f\"         Expected: {expected}, Got: {result}\")\n            failed += 1\n\n    return passed, failed\n\n\ndef test_validate_chmod():\n    \"\"\"Test chmod command validation.\"\"\"\n    print(\"\\nTesting chmod validation:\\n\")\n    passed = 0\n    failed = 0\n\n    # Test cases: (command, should_be_allowed, description)\n    test_cases = [\n        # Allowed cases\n        (\"chmod +x init.sh\", True, \"basic +x\"),\n        (\"chmod +x script.sh\", True, \"+x on any script\"),\n        (\"chmod u+x init.sh\", True, \"user +x\"),\n        (\"chmod a+x init.sh\", True, \"all +x\"),\n        (\"chmod ug+x init.sh\", True, \"user+group +x\"),\n        (\"chmod +x file1.sh file2.sh\", True, \"multiple files\"),\n        # Blocked cases\n        (\"chmod 777 init.sh\", False, \"numeric mode\"),\n        (\"chmod 755 init.sh\", False, \"numeric mode 755\"),\n        (\"chmod +w init.sh\", False, \"write permission\"),\n        (\"chmod +r init.sh\", False, \"read permission\"),\n        (\"chmod -x init.sh\", False, \"remove execute\"),\n        (\"chmod -R +x dir/\", False, \"recursive flag\"),\n        (\"chmod --recursive +x dir/\", False, \"long recursive flag\"),\n        (\"chmod +x\", False, \"missing file\"),\n    ]\n\n    for cmd, should_allow, description in test_cases:\n        allowed, reason = validate_chmod_command(cmd)\n        if allowed == should_allow:\n            print(f\"  PASS: {cmd!r} ({description})\")\n            passed += 1\n        else:\n            expected = \"allowed\" if should_allow else \"blocked\"\n            actual = \"allowed\" if allowed else \"blocked\"\n            print(f\"  FAIL: {cmd!r} ({description})\")\n            print(f\"         Expected: {expected}, Got: {actual}\")\n            if reason:\n                print(f\"         Reason: {reason}\")\n            failed += 1\n\n    return passed, failed\n\n\ndef test_validate_init_script():\n    \"\"\"Test init.sh script execution validation.\"\"\"\n    print(\"\\nTesting init.sh validation:\\n\")\n    passed = 0\n    failed = 0\n\n    # Test cases: (command, should_be_allowed, description)\n    test_cases = [\n        # Allowed cases\n        (\"./init.sh\", True, \"basic ./init.sh\"),\n        (\"./init.sh arg1 arg2\", True, \"with arguments\"),\n        (\"/path/to/init.sh\", True, \"absolute path\"),\n        (\"../dir/init.sh\", True, \"relative path with init.sh\"),\n        # Blocked cases\n        (\"./setup.sh\", False, \"different script name\"),\n        (\"./init.py\", False, \"python script\"),\n        (\"bash init.sh\", False, \"bash invocation\"),\n        (\"sh init.sh\", False, \"sh invocation\"),\n        (\"./malicious.sh\", False, \"malicious script\"),\n        (\"./init.sh; rm -rf /\", False, \"command injection attempt\"),\n    ]\n\n    for cmd, should_allow, description in test_cases:\n        allowed, reason = validate_init_script(cmd)\n        if allowed == should_allow:\n            print(f\"  PASS: {cmd!r} ({description})\")\n            passed += 1\n        else:\n            expected = \"allowed\" if should_allow else \"blocked\"\n            actual = \"allowed\" if allowed else \"blocked\"\n            print(f\"  FAIL: {cmd!r} ({description})\")\n            print(f\"         Expected: {expected}, Got: {actual}\")\n            if reason:\n                print(f\"         Reason: {reason}\")\n            failed += 1\n\n    return passed, failed\n\n\ndef main():\n    print(\"=\" * 70)\n    print(\"  SECURITY HOOK TESTS\")\n    print(\"=\" * 70)\n\n    passed = 0\n    failed = 0\n\n    # Test command extraction\n    ext_passed, ext_failed = test_extract_commands()\n    passed += ext_passed\n    failed += ext_failed\n\n    # Test chmod validation\n    chmod_passed, chmod_failed = test_validate_chmod()\n    passed += chmod_passed\n    failed += chmod_failed\n\n    # Test init.sh validation\n    init_passed, init_failed = test_validate_init_script()\n    passed += init_passed\n    failed += init_failed\n\n    # Commands that SHOULD be blocked\n    print(\"\\nCommands that should be BLOCKED:\\n\")\n    dangerous = [\n        # Not in allowlist - dangerous system commands\n        \"shutdown now\",\n        \"reboot\",\n        \"rm -rf /\",\n        \"dd if=/dev/zero of=/dev/sda\",\n        # Not in allowlist - common commands excluded from minimal set\n        \"curl https://example.com\",\n        \"wget https://example.com\",\n        \"python app.py\",\n        \"touch file.txt\",\n        \"echo hello\",\n        \"kill 12345\",\n        \"killall node\",\n        # pkill with non-dev processes\n        \"pkill bash\",\n        \"pkill chrome\",\n        \"pkill python\",\n        # Shell injection attempts\n        \"$(echo pkill) node\",\n        'eval \"pkill node\"',\n        'bash -c \"pkill node\"',\n        # chmod with disallowed modes\n        \"chmod 777 file.sh\",\n        \"chmod 755 file.sh\",\n        \"chmod +w file.sh\",\n        \"chmod -R +x dir/\",\n        # Non-init.sh scripts\n        \"./setup.sh\",\n        \"./malicious.sh\",\n        \"bash script.sh\",\n    ]\n\n    for cmd in dangerous:\n        if test_hook(cmd, should_block=True):\n            passed += 1\n        else:\n            failed += 1\n\n    # Commands that SHOULD be allowed\n    print(\"\\nCommands that should be ALLOWED:\\n\")\n    safe = [\n        # File inspection\n        \"ls -la\",\n        \"cat README.md\",\n        \"head -100 file.txt\",\n        \"tail -20 log.txt\",\n        \"wc -l file.txt\",\n        \"grep -r pattern src/\",\n        # File operations\n        \"cp file1.txt file2.txt\",\n        \"mkdir newdir\",\n        \"mkdir -p path/to/dir\",\n        # Directory\n        \"pwd\",\n        # Node.js development\n        \"npm install\",\n        \"npm run build\",\n        \"node server.js\",\n        # Version control\n        \"git status\",\n        \"git commit -m 'test'\",\n        \"git add . && git commit -m 'msg'\",\n        # Process management\n        \"ps aux\",\n        \"lsof -i :3000\",\n        \"sleep 2\",\n        # Allowed pkill patterns for dev servers\n        \"pkill node\",\n        \"pkill npm\",\n        \"pkill -f node\",\n        \"pkill -f 'node server.js'\",\n        \"pkill vite\",\n        # Chained commands\n        \"npm install && npm run build\",\n        \"ls | grep test\",\n        # Full paths\n        \"/usr/local/bin/node app.js\",\n        # chmod +x (allowed)\n        \"chmod +x init.sh\",\n        \"chmod +x script.sh\",\n        \"chmod u+x init.sh\",\n        \"chmod a+x init.sh\",\n        # init.sh execution (allowed)\n        \"./init.sh\",\n        \"./init.sh --production\",\n        \"/path/to/init.sh\",\n        # Combined chmod and init.sh\n        \"chmod +x init.sh && ./init.sh\",\n    ]\n\n    for cmd in safe:\n        if test_hook(cmd, should_block=False):\n            passed += 1\n        else:\n            failed += 1\n\n    # Summary\n    print(\"\\n\" + \"-\" * 70)\n    print(f\"  Results: {passed} passed, {failed} failed\")\n    print(\"-\" * 70)\n\n    if failed == 0:\n        print(\"\\n  ALL TESTS PASSED\")\n        return 0\n    else:\n        print(f\"\\n  {failed} TEST(S) FAILED\")\n        return 1\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n"
  },
  {
    "path": "browser-use-demo/.dockerignore",
    "content": "**/__pycache__\n**/*.pyc\n**/*.pyo\n**/*.pyd\n.Python\n*.egg-info/\n.git/\n.gitignore\n*.md\n.DS_Store\ntests/\n*.log\n.vscode/\n.idea/\n*.swp\n*.swo\n*~"
  },
  {
    "path": "browser-use-demo/.gitignore",
    "content": "# Python\n__pycache__/\n*.py[cod]\n*$py.class\n*.so\n.Python\nenv/\nvenv/\nENV/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# IDE\n.vscode/\n.idea/\n*.swp\n*.swo\n*~\n\n# OS\n.DS_Store\nThumbs.db\n\n# Logs\n*.log\n/tmp/\n\n# Environment\n.env\n.anthropic/\n\n# Test\n.pytest_cache/\n.coverage\nhtmlcov/\n\n# Streamlit\n.streamlit/cache/"
  },
  {
    "path": "browser-use-demo/CHANGELOG.md",
    "content": "# Modifications to Microsoft Playwright Source\n\nThis file tracks all modifications made to files derived from or inspired by Microsoft Playwright source code.\n\n## Modified Files\n\n### browser_use_demo/browser_tool_utils/browser_dom_script.js\n- **Date Modified**: 9/23/25\n- **Original Source**: https://github.com/microsoft/playwright/blob/main/packages/injected/src/ariaSnapshot.ts\n- **Nature of Changes**: Adapted Playwright's accessibility tree generation for use with browser tools API. Implemented accessibility tree extraction with element reference tracking, visibility filtering, and YAML-formatted output.\n\n### browser_use_demo/browser_tool_utils/browser_element_script.js\n- **Date Modified**: 9/23/25\n- **Original Source**: Microsoft Playwright element interaction patterns\n- **Nature of Changes**: Implemented element finding and interaction logic inspired by Playwright's approach to reliable element targeting and coordinate calculation.\n\n### browser_use_demo/tools/browser.py\n- **Date Modified**: 9/23/25\n- **Original Source**: Microsoft Playwright click emulation implementation\n- **Nature of Changes**: Click emulation methods developed with reference to Playwright source code during debugging to ensure reliable mouse interactions.\n- **Date Modified**: 10/6/25\n- **Nature of Changes**:\n  - Fixed incorrect path to browser_tool_utils directory. Changed from `Path(__file__).parent / \"browser_tool_utils\"` to `Path(__file__).parent.parent / \"browser_tool_utils\"` to correctly locate JavaScript utility files.\n  - Fixed missing `cdp_url` attribute initialization in `__init__` method to prevent AttributeError in cleanup method.\n  - Fixed incorrect import path for browser_key_map. Changed from `.browser_tool_utils.browser_key_map` to `..browser_tool_utils.browser_key_map` to correctly import the KEY_MAP.\n- **Date Modified**: 10/14/25\n- **Nature of Changes**:\n  - Enhanced `_scroll` and `_scroll_to` methods to return screenshots after scrolling actions. Added 0.5s stabilization delay before taking screenshots to show the new viewport content. This provides visual feedback to the model after scroll actions, consistent with the navigate action behavior.\n- **Date Modified**: 12/19/25\n- **Nature of Changes**:\n  - Added `hover` action to move mouse cursor without clicking using Playwright's `mouse.move()` API. Useful for revealing tooltips, dropdown menus, or triggering hover states.\n  - Added `execute_js` action to execute JavaScript code in page context using Playwright's `page.evaluate()` API. Returns the result of the last expression.\n- **Date Modified**: 1/18/26\n- **Nature of Changes**:\n  - Added clarifying comment in the `options` property explaining that this implementation uses fixed 1920x1080 dimensions with empirical coordinate correction, and directing users to the \"Handle coordinate scaling\" section in the computer use documentation for the recommended client-side downscaling approach.\n\n"
  },
  {
    "path": "browser-use-demo/Dockerfile",
    "content": "FROM docker.io/ubuntu:22.04\n\nENV DEBIAN_FRONTEND=noninteractive\nENV DEBIAN_PRIORITY=high\n\n# Install system dependencies\nRUN apt-get update && \\\n    apt-get -y upgrade && \\\n    apt-get -y install \\\n    # UI Requirements\n    xvfb \\\n    xterm \\\n    xdotool \\\n    scrot \\\n    imagemagick \\\n    sudo \\\n    mutter \\\n    x11vnc \\\n    # Python/pyenv reqs\n    build-essential \\\n    libssl-dev  \\\n    zlib1g-dev \\\n    libbz2-dev \\\n    libreadline-dev \\\n    libsqlite3-dev \\\n    curl \\\n    git \\\n    libncursesw5-dev \\\n    xz-utils \\\n    tk-dev \\\n    libxml2-dev \\\n    libxmlsec1-dev \\\n    libffi-dev \\\n    liblzma-dev \\\n    # Network tools\n    net-tools \\\n    netcat \\\n    # PPA req\n    software-properties-common && \\\n    # Browser and desktop apps\n    sudo apt-get install -y --no-install-recommends \\\n    x11-apps \\\n    tint2 \\\n    pcmanfm \\\n    unzip \\\n    # Playwright Chromium dependencies\n    libnss3 \\\n    libnspr4 \\\n    libatk1.0-0 \\\n    libatk-bridge2.0-0 \\\n    libcups2 \\\n    libatspi2.0-0 \\\n    libxcomposite1 \\\n    libxdamage1 \\\n    libxrandr2 \\\n    libgbm1 \\\n    libxkbcommon0 \\\n    libpango-1.0-0 \\\n    libcairo2 \\\n    libasound2 && \\\n    apt-get clean\n\n# Install noVNC\nRUN git clone --branch v1.5.0 https://github.com/novnc/noVNC.git /opt/noVNC && \\\n    git clone --branch v0.12.0 https://github.com/novnc/websockify /opt/noVNC/utils/websockify && \\\n    ln -s /opt/noVNC/vnc.html /opt/noVNC/index.html\n\n# Setup user\nENV USERNAME=browseruse\nENV HOME=/home/$USERNAME\nRUN useradd -m -s /bin/bash -d $HOME $USERNAME\nRUN echo \"${USERNAME} ALL=(ALL) NOPASSWD: ALL\" >> /etc/sudoers\nUSER browseruse\nWORKDIR $HOME\n\n# Setup Python\nRUN git clone https://github.com/pyenv/pyenv.git ~/.pyenv && \\\n    cd ~/.pyenv && src/configure && make -C src && cd .. && \\\n    echo 'export PYENV_ROOT=\"$HOME/.pyenv\"' >> ~/.bashrc && \\\n    echo 'command -v pyenv >/dev/null || export PATH=\"$PYENV_ROOT/bin:$PATH\"' >> ~/.bashrc && \\\n    echo 'eval \"$(pyenv init -)\"' >> ~/.bashrc\nENV PYENV_ROOT=\"$HOME/.pyenv\"\nENV PATH=\"$PYENV_ROOT/bin:$PATH\"\nENV PYENV_VERSION=3.11.6\nRUN eval \"$(pyenv init -)\" && \\\n    pyenv install $PYENV_VERSION && \\\n    pyenv global $PYENV_VERSION && \\\n    pyenv rehash\n\nENV PATH=\"$HOME/.pyenv/shims:$HOME/.pyenv/bin:$PATH\"\n\nRUN python -m pip install --upgrade pip==23.1.2 setuptools==58.0.4 wheel==0.40.0 && \\\n    python -m pip config set global.disable-pip-version-check true\n\n# Copy requirements and install dependencies\nCOPY --chown=$USERNAME:$USERNAME browser_use_demo/requirements.txt $HOME/browser_use_demo/requirements.txt\nRUN python -m pip install -r $HOME/browser_use_demo/requirements.txt\n\n# Install Playwright and Chromium\nRUN python -m playwright install chromium && \\\n    python -m playwright install-deps chromium\n\n# Setup desktop environment & app\nCOPY --chown=$USERNAME:$USERNAME image/ $HOME\nCOPY --chown=$USERNAME:$USERNAME browser_use_demo/ $HOME/browser_use_demo/\n\n# Display configuration - hardcoded values\nENV DISPLAY_NUM=1\nENV HEIGHT=1080\nENV WIDTH=1920\n\nENTRYPOINT [ \"./entrypoint.sh\" ]"
  },
  {
    "path": "browser-use-demo/NOTICE",
    "content": "This software includes components from the following open source projects:\n\nMicrosoft Playwright\nSource: https://github.com/microsoft/playwright\nLicense: Apache License 2.0\nCopyright (c) Microsoft Corporation\nModified files are marked with modification notices. See CHANGELOG.md for details."
  },
  {
    "path": "browser-use-demo/README.md",
    "content": "# Browser Automation Demo with Claude\n\nA complete reference implementation for building browser automation with Claude using Playwright. This demo provides a containerized Streamlit interface showcasing how to give Claude the ability to navigate websites, interact with DOM elements, extract content, and fill forms.\n\n## Overview\n\nThis demo implements a custom browser tool that enables Claude to interact with web browsers. It provides:\n\n- **DOM access**: Read page structure with element references\n- **Navigation control**: Browse URLs and manage browser history\n- **Form manipulation**: Directly set form input values\n- **Text extraction**: Get all text content from pages\n- **Element targeting**: Interact with elements via ref or coordinate parameters\n- **Smart scrolling**: Scroll to specific elements or in specific directions\n- **Page search**: Find and highlight text on pages\n- **Visual capture**: Take screenshots and capture zoomed regions\n\n### Advantages Over Coordinate-Based Automation\n\n- **Reliability**: Element-based targeting via the `ref` parameter works across different screen sizes and layouts, unlike pixel coordinates that break when windows resize\n- **Direct DOM manipulation**: Provides structured visibility into page elements and their properties, enabling precise interactions with dynamic content, hidden elements, and complex web applications\n- **Web-specific actions**: Built-in support for navigation, text extraction, and form completion\n\n## Quick Start\n\n### Prerequisites\n\n- Docker and Docker Compose installed on your system\n- Anthropic API key\n\n### Setup\n\n1. **Clone the repository**:\n   ```bash\n   git clone https://github.com/anthropics/claude-quickstarts.git\n   cd claude-quickstarts/browser-use-demo\n   ```\n\n2. **Configure environment**:\n   ```bash\n   cp .env.example .env\n   # Edit .env file and add your ANTHROPIC_API_KEY\n   ```\n\n   The display resolution is set to 1920x1080 (16:9) for optimal coordinate accuracy.\n   - See `.env.example` for more options and coordinate scaling details\n\n### Running with Docker Compose\n\n```bash\n# For production use:\ndocker-compose up --build\n\n# For development with file watching (auto-sync changes):\ndocker-compose up --build --watch\n```\n\n### Access the interfaces:\n   - **Main UI**: http://localhost:8080 (Streamlit interface)\n   - **NoVNC Browser View**: http://localhost:6080 (see the browser)\n   - **VNC**: Connect any VNC client to localhost:5900\n\nhttps://github.com/user-attachments/assets/4fb72078-6902-4b63-bcd1-5f2c4cd60582\n\n## Usage Examples\n\nOnce the demo is running, try these prompts in the Streamlit interface:\n\n- \"Navigate to news.ycombinator.com and tell me the top 3 stories\"\n- \"Go to google.com and search for 'Anthropic Claude'\"\n- \"Visit wikipedia.org and find information about artificial intelligence\"\n- \"Navigate to github.com and search for 'playwright'\"\n\nNote that the current Playwright implementation hits CAPTCHAs when searching Google.com. To avoid this, we recommend that you specify the website in the prompt (ie. navigate to Anthropic.com and search for x).\n\n## Safety Considerations\n\nBrowser automation poses unique risks that are distinct from standard API features or chat interfaces. These risks are heightened when using the tool to interact with the internet. To minimize risks, consider taking precautions such as:\n\n1. Run the browser in an isolated virtual machine or container environment with minimal privileges to prevent direct system attacks or accidents.\n2. Avoid giving the model access to sensitive data, such as account login information, to prevent information theft.\n3. Consider an allowlist of domains to reduce exposure to malicious content.\n4. Ask a human to confirm decisions that may result in meaningful real-world consequences as well as any tasks requiring affirmative consent, such as accepting cookies, executing financial transactions, or agreeing to terms of service.\n\nIn some circumstances, Claude will follow commands found in content even if it conflicts with the user's instructions. For example, instructions on webpages or contained in images may override user instructions or cause Claude to make mistakes. We suggest taking precautions to isolate Claude from sensitive data and actions to avoid risks related to prompt injection.\n\nFinally, please inform end users of relevant risks and obtain their consent prior to enabling browser automation in your own products.\n\nThis demo runs a browser in a containerized environment. While isolated, please note:\n\n- **Don't enter personal credentials or sensitive information** - This is a demonstration tool\n- **Be cautious about the websites you visit** - Some sites may have anti-automation measures\n- **Not for production use** - This demo is for learning and development purposes only\n\n## Implementation Reference\n\nThis demo shows how to build browser automation with Claude using Playwright. All browser actions (navigate, click, type, scroll, form_input, etc.) are implemented as methods in [browser.py](browser_use_demo/tools/browser.py) using Playwright's async API.\n\n### Key Files\n\n- **[browser.py](browser_use_demo/tools/browser.py)** - Main tool with all browser actions\n- **[loop.py](browser_use_demo/loop.py)** - Sampling loop for API calls and response handling\n- **[streamlit.py](browser_use_demo/streamlit.py)** - Chat UI\n- **[browser_tool_utils/](browser_use_demo/browser_tool_utils/)** - JavaScript utilities for DOM extraction, element finding, and form manipulation\n\n### Core Patterns\n\n**Element references:** JavaScript utilities generate `ref` identifiers for reliable element targeting across screen sizes (replacing brittle pixel coordinates).\n\n**Tool setup:**\n```python\nbrowser_tool = BrowserTool()\n\ndef to_params(self):\n    return {\n        \"name\": \"browser\",\n        \"description\": BROWSER_TOOL_DESCRIPTION,\n        \"input_schema\": BROWSER_TOOL_INPUT_SCHEMA,\n    }\n```\n\n### Coordinate Scaling\n\nThe browser tools implementation includes automatic coordinate scaling to ensure accurate interactions:\n\n**How it works:**\n- The browser viewport is fixed at 1920x1080 (16:9 aspect ratio)\n- Claude processes screenshots at 1456x819 pixels for 16:9 aspect ratio (see [documentation](https://docs.claude.com/en/docs/build-with-claude/vision#evaluate-image-size))\n- The browser tool automatically scales coordinates from Claude's processed resolution (1456x819) to the actual viewport (1920x1080)\n- This ensures clicks and interactions happen at the correct locations\n\nSee `browser_use_demo/tools/coordinate_scaling.py` for the implementation.\n\nThis demo uses a custom tool definition with an explicit input schema, giving you full control over the tool interface. The `BROWSER_TOOL_DESCRIPTION` and `BROWSER_TOOL_INPUT_SCHEMA` constants in [browser.py](browser_use_demo/tools/browser.py) provide a complete example you can use as a starting point for your own browser automation tools.\n\n\n### Modifying & Using as a Template\n\n**To modify this demo:**\n1. Edit `browser_use_demo/tools/browser.py` to add features or change behavior\n2. Rebuild the Docker image (volume mount allows live Python code updates)\n\n**To use as a template for your own project:**\n1. Copy [browser.py](browser_use_demo/tools/browser.py) and [browser_tool_utils/](browser_use_demo/browser_tool_utils/)\n2. Adapt [loop.py](browser_use_demo/loop.py) for your API integration\n3. Build your UI or use [streamlit.py](browser_use_demo/streamlit.py) as a starting point\n\n## Architecture\n\n```\n┌──────────────────────────────────┐\n│     Docker Container              │\n│                                   │\n│  ┌─────────────────────────────┐ │\n│  │   Streamlit Interface       │ │  ← User interacts here\n│  └──────────┬──────────────────┘ │\n│             │                     │\n│  ┌──────────▼──────────────────┐ │\n│  │  Claude API + Browser Tool  │ │  ← Claude controls browser\n│  └──────────┬──────────────────┘ │\n│             │                     │\n│  ┌──────────▼──────────────────┐ │\n│  │   Playwright + Chromium     │ │  ← Browser automation\n│  └──────────┬──────────────────┘ │\n│             │                     │\n│  ┌──────────▼──────────────────┐ │\n│  │   XVFB Virtual Display      │ │  ← Virtual display\n│  └──────────┬──────────────────┘ │\n│             │                     │\n│  ┌──────────▼──────────────────┐ │\n│  │   VNC/NoVNC Server          │ │  ← Visual access\n│  └─────────────────────────────┘ │\n└──────────────────────────────────┘\n```\n\n## How Browser Automation Differs from Computer Use\n\nThis browser automation demo is specifically optimized for web automation with DOM-aware features like element targeting, page reading, and form manipulation. While it shares many capabilities with the [computer use demo](../computer-use-demo), browser automation adds web-specific actions and the ability to target elements by reference instead of just coordinates. Computer use provides general desktop control for any application, while browser automation focuses exclusively on browser-based tasks.\n\n### Actions Unique to Browser Automation\n\nThese web-specific actions are not available in computer use:\n\n- **navigate**: Navigate to URL or use \"back\"/\"forward\" for history (requires text)\n- **read_page**: Get DOM tree with element refs; use text=\"interactive\" to filter\n- **get_page_text**: Extract all text content from the page\n- **find**: Search for text and highlight matches (requires text)\n- **form_input**: Set form element value directly (requires ref and value)\n- **scroll_to**: Scroll element into view (requires ref)\n- **execute_js**: Run JavaScript code in page context (requires text with JS code)\n\n### Actions Shared with Computer Use\n\nThese actions work similarly to their computer use counterparts. The key difference is that browser automation allows targeting by element reference (`ref`) as an alternative to coordinates:\n\n**Mouse Actions** (accept either `ref` or `coordinate`):\n- **left_click**, **right_click**, **middle_click**, **double_click**, **triple_click**\n- **hover**: Move cursor without clicking (for tooltips, dropdowns)\n- **left_click_drag**: Drag from start_coordinate to coordinate\n- **left_mouse_down**, **left_mouse_up**: Fine-grained mouse control\n\n**Keyboard Actions**:\n- **type**: Type text at cursor (requires text)\n- **key**: Press key or combination (requires text)\n- **hold_key**: Hold key for duration (requires text and duration)\n\n**Other**:\n- **screenshot**: Capture current viewport\n- **scroll**: Scroll in direction (requires scroll_direction, scroll_amount, coordinate)\n- **zoom**: Zoomed screenshot of region (requires region: x1, y1, x2, y2)\n- **wait**: Wait for duration in seconds (requires duration, 0-100)\n\n### Computer Use Actions Not Included\n\nThese desktop-level actions from computer use are not in this browser demo:\n\n- **cursor_position**: Get current (x, y) pixel coordinate of cursor\n\nThis is less relevant for browser automation since the `ref` parameter provides reliable element-based targeting, replacing the need for cursor tracking. Note that `hover` provides similar functionality to `mouse_move` for triggering hover states.\n\n\n## Troubleshooting\n\n**Browser not visible?**\n- Check that port 6080 is accessible\n- Try refreshing the NoVNC page\n- Ensure Docker has sufficient resources allocated\n\n**API errors?**\n- Verify your Anthropic API key is set correctly\n- Check you're using a compatible model (Claude 4.5 models: claude-sonnet-4-5-20250929, claude-opus-4-5-20251101, or claude-haiku-4-5-20251001)\n\n**Browser actions failing?**\n- Some websites may have anti-automation measures\n- Try simpler websites first to test functionality\n- Check the browser view to see what's happening\n\n## Attribution\n\nThis software includes components from Microsoft Playwright. See the [NOTICE](NOTICE) file for details.\n\n## Credits\n\nBuilt with:\n- [Anthropic Claude API](https://www.anthropic.com)\n- [Playwright](https://playwright.dev)\n- [Streamlit](https://streamlit.io)\n- [NoVNC](https://novnc.com)\n"
  },
  {
    "path": "browser-use-demo/browser_use_demo/__init__.py",
    "content": "\"\"\"Browser Use Demo - Web automation with Claude.\"\"\"\n\n__version__ = \"0.1.0\""
  },
  {
    "path": "browser-use-demo/browser_use_demo/browser_tool_utils/__init__.py",
    "content": "# Browser tool utility files\n"
  },
  {
    "path": "browser-use-demo/browser_use_demo/browser_tool_utils/browser_dom_script.js",
    "content": "/*\n * Modifications Copyright (c) 2025 Anthropic, PBC\n * Modified from original Microsoft Playwright source\n * Original Microsoft Playwright source licensed under Apache License 2.0\n * See CHANGELOG.md for details\n */\n\n// Content script that defines the accessibility tree generation function in the MAIN context\n\n(function () {\n  // Initialize global element map and ref counter if not already present\n  if (!window.__claudeElementMap) {\n    window.__claudeElementMap = {};\n  }\n  if (!window.__claudeRefCounter) {\n    window.__claudeRefCounter = 0;\n  }\n\n  // Define the accessibility tree generation function on the window (in content script context)\n  window.__generateAccessibilityTree = function (filterType) {\n    try {\n      var result = [];\n\n      function getRole(element) {\n        var role = element.getAttribute(\"role\");\n        if (role) return role;\n\n        var tag = element.tagName.toLowerCase();\n        var type = element.getAttribute(\"type\");\n\n        var roleMap = {\n          a: \"link\",\n          button: \"button\",\n          input:\n            type === \"submit\" || type === \"button\"\n              ? \"button\"\n              : type === \"checkbox\"\n                ? \"checkbox\"\n                : type === \"radio\"\n                  ? \"radio\"\n                  : type === \"file\"\n                    ? \"button\"\n                    : \"textbox\",\n          select: \"combobox\",\n          textarea: \"textbox\",\n          h1: \"heading\",\n          h2: \"heading\",\n          h3: \"heading\",\n          h4: \"heading\",\n          h5: \"heading\",\n          h6: \"heading\",\n          img: \"image\",\n          nav: \"navigation\",\n          main: \"main\",\n          header: \"banner\",\n          footer: \"contentinfo\",\n          section: \"region\",\n          article: \"article\",\n          aside: \"complementary\",\n          form: \"form\",\n          table: \"table\",\n          ul: \"list\",\n          ol: \"list\",\n          li: \"listitem\",\n          label: \"label\",\n        };\n\n        return roleMap[tag] || \"generic\";\n      }\n\n      function getCleanName(element) {\n        var tag = element.tagName.toLowerCase();\n\n        // For selects, get the selected option text\n        if (tag === \"select\") {\n          var selectElement = element;\n          var selectedOption =\n            selectElement.querySelector(\"option[selected]\") ||\n            selectElement.options[selectElement.selectedIndex];\n          if (selectedOption && selectedOption.textContent) {\n            return selectedOption.textContent.trim();\n          }\n        }\n\n        // Priority order for getting meaningful names\n        var ariaLabel = element.getAttribute(\"aria-label\");\n        if (ariaLabel && ariaLabel.trim()) return ariaLabel.trim();\n\n        var placeholder = element.getAttribute(\"placeholder\");\n        if (placeholder && placeholder.trim()) return placeholder.trim();\n\n        var title = element.getAttribute(\"title\");\n        if (title && title.trim()) return title.trim();\n\n        var alt = element.getAttribute(\"alt\");\n        if (alt && alt.trim()) return alt.trim();\n\n        // For form labels\n        if (element.id) {\n          var label = document.querySelector('label[for=\"' + element.id + '\"]');\n          if (label && label.textContent && label.textContent.trim()) {\n            return label.textContent.trim();\n          }\n        }\n\n        // For inputs with values\n        if (tag === \"input\") {\n          var inputElement = element;\n          var type = element.getAttribute(\"type\") || \"\";\n          var value = element.getAttribute(\"value\");\n\n          if (type === \"submit\" && value && value.trim()) {\n            return value.trim();\n          }\n\n          if (\n            inputElement.value &&\n            inputElement.value.length < 50 &&\n            inputElement.value.trim()\n          ) {\n            return inputElement.value.trim();\n          }\n        }\n\n        // For buttons, links, and other interactive elements, get direct text\n        if ([\"button\", \"a\", \"summary\"].includes(tag)) {\n          var directText = \"\";\n          for (var i = 0; i < element.childNodes.length; i++) {\n            var node = element.childNodes[i];\n            if (node.nodeType === Node.TEXT_NODE) {\n              directText += node.textContent;\n            }\n          }\n          if (directText.trim()) return directText.trim();\n        }\n\n        // For headings, get text content but limit it\n        if (tag.match(/^h[1-6]$/)) {\n          var headingText = element.textContent;\n          if (headingText && headingText.trim()) {\n            return headingText.trim().substring(0, 100);\n          }\n        }\n\n        // For images without alt, try to get surrounding context\n        if (tag === \"img\") {\n          var src = element.getAttribute(\"src\");\n          if (src) {\n            var filename = src.split(\"/\").pop()?.split(\"?\")[0];\n            return \"Image: \" + filename;\n          }\n        }\n\n        // For generic elements, get direct text content (not including child elements)\n        // This helps capture important text in spans, divs, etc.\n        var directTextContent = \"\";\n        for (var j = 0; j < element.childNodes.length; j++) {\n          var childNode = element.childNodes[j];\n          if (childNode.nodeType === Node.TEXT_NODE) {\n            directTextContent += childNode.textContent;\n          }\n        }\n\n        if (\n          directTextContent &&\n          directTextContent.trim() &&\n          directTextContent.trim().length >= 3\n        ) {\n          // Only return if it's meaningful text (at least 3 characters)\n          var trimmedText = directTextContent.trim();\n          if (trimmedText.length > 50) {\n            return trimmedText.substring(0, 50) + \"...\";\n          }\n          return trimmedText;\n        }\n\n        return \"\";\n      }\n\n      function isVisible(element) {\n        var style = window.getComputedStyle(element);\n        return (\n          style.display !== \"none\" &&\n          style.visibility !== \"hidden\" &&\n          style.opacity !== \"0\" &&\n          element.offsetWidth > 0 &&\n          element.offsetHeight > 0\n        );\n      }\n\n      function isInteractive(element) {\n        var tag = element.tagName.toLowerCase();\n        var interactiveTags = [\n          \"a\",\n          \"button\",\n          \"input\",\n          \"select\",\n          \"textarea\",\n          \"details\",\n          \"summary\",\n        ];\n\n        return (\n          interactiveTags.includes(tag) ||\n          element.getAttribute(\"onclick\") !== null ||\n          element.getAttribute(\"tabindex\") !== null ||\n          element.getAttribute(\"role\") === \"button\" ||\n          element.getAttribute(\"role\") === \"link\" ||\n          element.getAttribute(\"contenteditable\") === \"true\"\n        );\n      }\n\n      function isSemantic(element) {\n        var tag = element.tagName.toLowerCase();\n        var semanticTags = [\n          \"h1\",\n          \"h2\",\n          \"h3\",\n          \"h4\",\n          \"h5\",\n          \"h6\",\n          \"nav\",\n          \"main\",\n          \"header\",\n          \"footer\",\n          \"section\",\n          \"article\",\n          \"aside\",\n        ];\n        return (\n          semanticTags.includes(tag) || element.getAttribute(\"role\") !== null\n        );\n      }\n\n      function shouldIncludeElement(element, options) {\n        var tag = element.tagName.toLowerCase();\n\n        // Always skip these\n        if (\n          [\"script\", \"style\", \"meta\", \"link\", \"title\", \"noscript\"].includes(tag)\n        )\n          return false;\n        if (element.getAttribute(\"aria-hidden\") === \"true\") return false;\n\n        // Always check visibility - this is now mandatory\n        if (!isVisible(element)) return false;\n\n        // Check viewport visibility for all elements (unless using 'all' filter for find tool)\n        if (options.filter !== \"all\") {\n          var rect = element.getBoundingClientRect();\n          var inViewport =\n            rect.top < window.innerHeight &&\n            rect.bottom > 0 &&\n            rect.left < window.innerWidth &&\n            rect.right > 0;\n          if (!inViewport) return false;\n        }\n\n        // Apply interactive filter if specified\n        if (options.filter === \"interactive\") {\n          return isInteractive(element);\n        }\n\n        // Default behavior when no filter is specified (all visible elements)\n        // Always include interactive elements\n        if (isInteractive(element)) return true;\n\n        // Always include semantic elements (headings, nav, etc.)\n        if (isSemantic(element)) return true;\n\n        // Include elements with meaningful text content\n        if (getCleanName(element).length > 0) return true;\n\n        // For generic divs and spans, be more selective but still include text-containing ones\n        var role = getRole(element);\n        if (role === \"generic\" && (tag === \"div\" || tag === \"span\")) {\n          var id = element.id || \"\";\n          var className = element.className || \"\";\n          var cleanName = getCleanName(element);\n\n          // Include if it has meaningful text content (now that we extract text better)\n          if (cleanName && cleanName.length >= 3) {\n            return true;\n          }\n\n          // Only keep divs/spans that are clearly functional containers (not layout)\n          var functionalKeywords = [\n            \"search\",\n            \"dropdown\",\n            \"menu\",\n            \"modal\",\n            \"dialog\",\n            \"popup\",\n            \"toolbar\",\n            \"sidebar\",\n            \"content\",\n            \"text\",\n          ];\n          var isFunctionalContainer = functionalKeywords.some(\n            function (keyword) {\n              return id.includes(keyword) || className.includes(keyword);\n            },\n          );\n\n          if (isFunctionalContainer) {\n            return true;\n          }\n\n          // Skip empty generic containers - they're just layout noise\n          return false;\n        }\n\n        // Include other container elements that might have interactive children\n        if (isContainerElement(element)) return true;\n\n        return false;\n      }\n\n      function isContainerElement(element) {\n        var role = element.getAttribute(\"role\");\n        var tag = element.tagName.toLowerCase();\n        var className = element.className || \"\";\n        var id = element.id || \"\";\n\n        // These are containers that should be traversed deeper\n        return (\n          role === \"search\" ||\n          role === \"form\" ||\n          role === \"group\" ||\n          role === \"toolbar\" ||\n          role === \"navigation\" ||\n          tag === \"form\" ||\n          tag === \"fieldset\" ||\n          tag === \"nav\" ||\n          // Generic functional containers\n          id.includes(\"search\") ||\n          className.includes(\"search\") ||\n          id.includes(\"form\") ||\n          className.includes(\"form\") ||\n          id.includes(\"menu\") ||\n          className.includes(\"menu\") ||\n          id.includes(\"nav\") ||\n          className.includes(\"nav\")\n        );\n      }\n\n      function processElement(element, depth, options) {\n        if (depth > 15) return; // Generous depth limit for very complex pages\n        if (!element || !element.tagName) return;\n\n        var shouldInclude = shouldIncludeElement(element, options);\n        var actuallyInclude = shouldInclude || depth === 0; // Always include root (body)\n\n        if (actuallyInclude) {\n          var role = getRole(element);\n          var name = getCleanName(element);\n          var ref = null;\n\n          // Check if this element already has a ref in the global map\n          for (var existingRef in window.__claudeElementMap) {\n            var weakRef = window.__claudeElementMap[existingRef];\n            var existingElement = weakRef.deref();\n            if (existingElement === element) {\n              ref = existingRef;\n              break;\n            }\n          }\n\n          // If not found, create a new ref\n          if (!ref) {\n            ref = \"ref_\" + ++window.__claudeRefCounter;\n            window.__claudeElementMap[ref] = new WeakRef(element);\n          }\n\n          var indent = \"  \".repeat(depth);\n          var yaml = indent + \"- \" + role;\n\n          if (name) {\n            // Clean up the name - remove newlines, limit length\n            name = name.replace(/\\s+/g, \" \").substring(0, 100);\n            yaml += ' \"' + name.replace(/\"/g, '\\\\\"') + '\"';\n          }\n\n          yaml += \" [ref=\" + ref + \"]\";\n\n          // Add useful attributes\n          if (element.id) yaml += ' id=\"' + element.id + '\"';\n          if (element.getAttribute(\"href\"))\n            yaml += ' href=\"' + element.getAttribute(\"href\") + '\"';\n          if (element.getAttribute(\"type\"))\n            yaml += ' type=\"' + element.getAttribute(\"type\") + '\"';\n          if (element.getAttribute(\"placeholder\"))\n            yaml +=\n              ' placeholder=\"' + element.getAttribute(\"placeholder\") + '\"';\n\n          result.push(yaml);\n        }\n\n        // Always traverse children - we need to go deep to find interactive elements\n        if (element.children && depth < 15) {\n          for (var i = 0; i < element.children.length; i++) {\n            processElement(\n              element.children[i],\n              actuallyInclude ? depth + 1 : depth,\n              options,\n            );\n          }\n        }\n      }\n\n      var options = {\n        filter: filterType,\n      };\n\n      if (document.body) {\n        processElement(document.body, 0, options);\n      }\n\n      // Clean up stale references (elements that have been garbage collected)\n      for (var ref in window.__claudeElementMap) {\n        var weakRef = window.__claudeElementMap[ref];\n        if (!weakRef.deref()) {\n          delete window.__claudeElementMap[ref];\n        }\n      }\n\n      // Filter out empty generic elements\n      var filteredResult = result.filter(function (line) {\n        return !/^\\s*- generic \\[ref=ref_\\d+\\]$/.test(line);\n      });\n\n      return {\n        pageContent: filteredResult.join(\"\\n\"),\n        viewport: {\n          width: window.innerWidth,\n          height: window.innerHeight,\n        },\n      };\n    } catch (error) {\n      console.error(\"Error in accessibility tree generation:\", error);\n      throw new Error(\n        \"Error generating accessibility tree: \" +\n          (error.message || \"Unknown error\"),\n      );\n    }\n  };\n})();"
  },
  {
    "path": "browser-use-demo/browser_use_demo/browser_tool_utils/browser_element_script.js",
    "content": "/*\n * Modifications Copyright (c) 2025 Anthropic, PBC\n * Modified from original Microsoft Playwright source\n * Original Microsoft Playwright source licensed under Apache License 2.0\n * See CHANGELOG.md for details\n */\n\n// Script for interacting with elements by their reference IDs\n\n(function(elementRef) {\n    try {\n        // Get element from reference map\n        let targetElement = null;\n        \n        if (window.__claudeElementMap && window.__claudeElementMap[elementRef]) {\n            const weakRef = window.__claudeElementMap[elementRef];\n            targetElement = weakRef.deref() || null;\n            \n            if (!targetElement || !document.contains(targetElement)) {\n                // Element has been removed from DOM\n                delete window.__claudeElementMap[elementRef];\n                targetElement = null;\n            }\n        }\n        \n        if (!targetElement) {\n            return {\n                success: false,\n                action: 'get_element',\n                message: `No element found with reference: \"${elementRef}\". The element may have been removed from the page.`\n            };\n        }\n        \n        // Scroll element into view if needed\n        targetElement.scrollIntoView({ behavior: 'instant', block: 'center', inline: 'center' });\n        \n        // Force a layout/paint to ensure the element is properly positioned after scroll\n        targetElement.offsetHeight;\n        \n        // Get element coordinates\n        const rect = targetElement.getBoundingClientRect();\n        const clickX = rect.left + rect.width / 2;\n        const clickY = rect.top + rect.height / 2;\n        \n        // Build element info string\n        const elementInfo = targetElement.tagName.toLowerCase() + \n            (targetElement.id ? '#' + targetElement.id : '') +\n            (targetElement.className ? '.' + targetElement.className.split(' ').filter(c => c).join('.') : '');\n        \n        // Get additional element properties\n        const elementType = targetElement.getAttribute('type') || '';\n        const elementRole = targetElement.getAttribute('role') || '';\n        const elementAriaLabel = targetElement.getAttribute('aria-label') || '';\n        const elementText = targetElement.textContent ? targetElement.textContent.substring(0, 100) : '';\n        \n        return {\n            success: true,\n            coordinates: [clickX, clickY],\n            elementInfo: elementInfo,\n            elementRef: elementRef,\n            rect: {\n                left: rect.left,\n                top: rect.top,\n                right: rect.right,\n                bottom: rect.bottom,\n                width: rect.width,\n                height: rect.height\n            },\n            attributes: {\n                type: elementType,\n                role: elementRole,\n                ariaLabel: elementAriaLabel,\n                text: elementText\n            },\n            isVisible: rect.width > 0 && rect.height > 0,\n            isInteractable: !targetElement.disabled && \n                           targetElement.style.display !== 'none' &&\n                           targetElement.style.visibility !== 'hidden'\n        };\n    } catch (error) {\n        return {\n            success: false,\n            action: 'get_element',\n            message: 'Error finding element by reference: ' + (error.message || 'Unknown error')\n        };\n    }\n})"
  },
  {
    "path": "browser-use-demo/browser_use_demo/browser_tool_utils/browser_form_input_script.js",
    "content": "// Script for setting values in form elements by their reference IDs\n\n(function(elementRef, inputValue) {\n    try {\n        // Get element from reference map\n        let element = null;\n        \n        if (window.__claudeElementMap && window.__claudeElementMap[elementRef]) {\n            const weakRef = window.__claudeElementMap[elementRef];\n            element = weakRef.deref() || null;\n            \n            if (!element || !document.contains(element)) {\n                // Element has been removed from DOM\n                delete window.__claudeElementMap[elementRef];\n                element = null;\n            }\n        }\n        \n        if (!element) {\n            return {\n                success: false,\n                action: 'form_input',\n                message: `No element found with reference: \"${elementRef}\". The element may have been removed from the page.`\n            };\n        }\n        \n        // Scroll element into view\n        element.scrollIntoView({ behavior: 'smooth', block: 'center' });\n        \n        // Handle different element types\n        if (element instanceof HTMLSelectElement) {\n            const previousValue = element.value;\n            const options = Array.from(element.options);\n            \n            // Try to find option by value or text\n            let optionFound = false;\n            const valueStr = String(inputValue);\n            \n            for (let i = 0; i < options.length; i++) {\n                if (options[i].value === valueStr || options[i].text === valueStr) {\n                    element.selectedIndex = i;\n                    optionFound = true;\n                    break;\n                }\n            }\n            \n            if (!optionFound) {\n                return {\n                    success: false,\n                    action: 'form_input',\n                    message: `Option \"${valueStr}\" not found. Available options: ${options.map(o => `\"${o.text}\" (value: \"${o.value}\")`).join(', ')}`\n                };\n            }\n            \n            // Focus and dispatch events\n            element.focus();\n            element.dispatchEvent(new Event('change', { bubbles: true }));\n            element.dispatchEvent(new Event('input', { bubbles: true }));\n            \n            return {\n                success: true,\n                action: 'form_input',\n                ref: elementRef,\n                element_type: 'select',\n                previous_value: previousValue,\n                new_value: element.value,\n                message: `Selected option \"${valueStr}\" in dropdown`\n            };\n        } else if (element instanceof HTMLInputElement && element.type === 'checkbox') {\n            const previousValue = element.checked;\n            \n            if (typeof inputValue !== 'boolean') {\n                return {\n                    success: false,\n                    action: 'form_input',\n                    message: 'Checkbox requires a boolean value (true/false)'\n                };\n            }\n            \n            element.checked = inputValue;\n            element.focus();\n            element.dispatchEvent(new Event('change', { bubbles: true }));\n            element.dispatchEvent(new Event('input', { bubbles: true }));\n            \n            return {\n                success: true,\n                action: 'form_input',\n                ref: elementRef,\n                element_type: 'checkbox',\n                previous_value: previousValue,\n                new_value: element.checked,\n                message: `Checkbox ${element.checked ? 'checked' : 'unchecked'}`\n            };\n        } else if (element instanceof HTMLInputElement && element.type === 'radio') {\n            const previousValue = element.checked;\n            const radioGroup = element.name;\n            \n            // For radio buttons, we always set to true (can't uncheck a radio by clicking)\n            element.checked = true;\n            element.focus();\n            element.dispatchEvent(new Event('change', { bubbles: true }));\n            element.dispatchEvent(new Event('input', { bubbles: true }));\n            \n            return {\n                success: true,\n                action: 'form_input',\n                ref: elementRef,\n                element_type: 'radio',\n                previous_value: previousValue,\n                new_value: element.checked,\n                message: `Radio button selected${radioGroup ? ` in group \"${radioGroup}\"` : ''}`\n            };\n        } else if (element instanceof HTMLInputElement && \n                   (element.type === 'date' || element.type === 'time' || \n                    element.type === 'datetime-local' || element.type === 'month' || \n                    element.type === 'week')) {\n            const previousValue = element.value;\n            element.value = String(inputValue);\n            element.focus();\n            element.dispatchEvent(new Event('change', { bubbles: true }));\n            element.dispatchEvent(new Event('input', { bubbles: true }));\n            \n            return {\n                success: true,\n                action: 'form_input',\n                ref: elementRef,\n                element_type: element.type,\n                previous_value: previousValue,\n                new_value: element.value,\n                message: `Set ${element.type} to \"${element.value}\"`\n            };\n        } else if (element instanceof HTMLInputElement && element.type === 'range') {\n            const previousValue = element.value;\n            const numValue = Number(inputValue);\n            \n            if (isNaN(numValue)) {\n                return {\n                    success: false,\n                    action: 'form_input',\n                    message: 'Range input requires a numeric value'\n                };\n            }\n            \n            element.value = String(numValue);\n            element.focus();\n            element.dispatchEvent(new Event('change', { bubbles: true }));\n            element.dispatchEvent(new Event('input', { bubbles: true }));\n            \n            return {\n                success: true,\n                action: 'form_input',\n                ref: elementRef,\n                element_type: 'range',\n                previous_value: previousValue,\n                new_value: element.value,\n                message: `Set range to ${element.value} (min: ${element.min}, max: ${element.max})`\n            };\n        } else if (element instanceof HTMLInputElement && element.type === 'number') {\n            const previousValue = element.value;\n            const numValue = Number(inputValue);\n            \n            if (isNaN(numValue) && inputValue !== '') {\n                return {\n                    success: false,\n                    action: 'form_input',\n                    message: 'Number input requires a numeric value'\n                };\n            }\n            \n            element.value = String(inputValue);\n            element.focus();\n            element.dispatchEvent(new Event('change', { bubbles: true }));\n            element.dispatchEvent(new Event('input', { bubbles: true }));\n            \n            return {\n                success: true,\n                action: 'form_input',\n                ref: elementRef,\n                element_type: 'number',\n                previous_value: previousValue,\n                new_value: element.value,\n                message: `Set number input to ${element.value}`\n            };\n        } else if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {\n            const previousValue = element.value;\n            element.value = String(inputValue);\n            element.focus();\n            \n            // Set cursor position to end\n            element.setSelectionRange(element.value.length, element.value.length);\n            \n            element.dispatchEvent(new Event('change', { bubbles: true }));\n            element.dispatchEvent(new Event('input', { bubbles: true }));\n            \n            const elementType = element instanceof HTMLTextAreaElement ? 'textarea' : (element.type || 'text');\n            \n            return {\n                success: true,\n                action: 'form_input',\n                ref: elementRef,\n                element_type: elementType,\n                previous_value: previousValue,\n                new_value: element.value,\n                message: `Set ${elementType} value to \"${element.value}\"`\n            };\n        } else {\n            return {\n                success: false,\n                action: 'form_input',\n                message: `Element type \"${element.tagName}\" is not a supported form input`\n            };\n        }\n    } catch (error) {\n        return {\n            success: false,\n            action: 'form_input',\n            message: `Error setting form value: ${error.message || 'Unknown error'}`\n        };\n    }\n})"
  },
  {
    "path": "browser-use-demo/browser_use_demo/browser_tool_utils/browser_key_map.py",
    "content": "\"\"\"Key mapping for browser keyboard input via Chrome DevTools Protocol.\"\"\"\n\nfrom typing import TypedDict\n\n\nclass KeyInfo(TypedDict, total=False):\n    key: str\n    code: str\n    keyCode: int\n    text: str\n    isKeypad: bool\n\n\nKEY_MAP: dict[str, KeyInfo] = {\n    # Modifier keys (for key combinations like ctrl+a, cmd+c)\n    \"ctrl\": {\"key\": \"Control\", \"code\": \"ControlLeft\", \"keyCode\": 17},\n    \"control\": {\"key\": \"Control\", \"code\": \"ControlLeft\", \"keyCode\": 17},\n    \"cmd\": {\"key\": \"Meta\", \"code\": \"MetaLeft\", \"keyCode\": 91},\n    \"command\": {\"key\": \"Meta\", \"code\": \"MetaLeft\", \"keyCode\": 91},\n    \"meta\": {\"key\": \"Meta\", \"code\": \"MetaLeft\", \"keyCode\": 91},\n    \"alt\": {\"key\": \"Alt\", \"code\": \"AltLeft\", \"keyCode\": 18},\n    \"option\": {\"key\": \"Alt\", \"code\": \"AltLeft\", \"keyCode\": 18},\n    \"shift\": {\"key\": \"Shift\", \"code\": \"ShiftLeft\", \"keyCode\": 16},\n    # Enter keys\n    \"enter\": {\"key\": \"Enter\", \"code\": \"Enter\", \"keyCode\": 13, \"text\": \"\\r\"},\n    \"return\": {\"key\": \"Enter\", \"code\": \"Enter\", \"keyCode\": 13, \"text\": \"\\r\"},\n    \"kp_enter\": {\n        \"key\": \"Enter\",\n        \"code\": \"Enter\",\n        \"keyCode\": 13,\n        \"text\": \"\\r\",\n        \"isKeypad\": True,\n    },\n    # Navigation keys\n    \"tab\": {\"key\": \"Tab\", \"code\": \"Tab\", \"keyCode\": 9},\n    \"delete\": {\"key\": \"Delete\", \"code\": \"Delete\", \"keyCode\": 46},\n    \"backspace\": {\"key\": \"Backspace\", \"code\": \"Backspace\", \"keyCode\": 8},\n    \"escape\": {\"key\": \"Escape\", \"code\": \"Escape\", \"keyCode\": 27},\n    \"esc\": {\"key\": \"Escape\", \"code\": \"Escape\", \"keyCode\": 27},\n    \"space\": {\"key\": \" \", \"code\": \"Space\", \"keyCode\": 32, \"text\": \" \"},\n    \" \": {\"key\": \" \", \"code\": \"Space\", \"keyCode\": 32, \"text\": \" \"},\n    # Arrow keys\n    \"arrowup\": {\"key\": \"ArrowUp\", \"code\": \"ArrowUp\", \"keyCode\": 38},\n    \"arrowdown\": {\"key\": \"ArrowDown\", \"code\": \"ArrowDown\", \"keyCode\": 40},\n    \"arrowleft\": {\"key\": \"ArrowLeft\", \"code\": \"ArrowLeft\", \"keyCode\": 37},\n    \"arrowright\": {\"key\": \"ArrowRight\", \"code\": \"ArrowRight\", \"keyCode\": 39},\n    \"up\": {\"key\": \"ArrowUp\", \"code\": \"ArrowUp\", \"keyCode\": 38},\n    \"down\": {\"key\": \"ArrowDown\", \"code\": \"ArrowDown\", \"keyCode\": 40},\n    \"left\": {\"key\": \"ArrowLeft\", \"code\": \"ArrowLeft\", \"keyCode\": 37},\n    \"right\": {\"key\": \"ArrowRight\", \"code\": \"ArrowRight\", \"keyCode\": 39},\n    # Page navigation\n    \"home\": {\"key\": \"Home\", \"code\": \"Home\", \"keyCode\": 36},\n    \"end\": {\"key\": \"End\", \"code\": \"End\", \"keyCode\": 35},\n    \"pageup\": {\"key\": \"PageUp\", \"code\": \"PageUp\", \"keyCode\": 33},\n    \"pagedown\": {\"key\": \"PageDown\", \"code\": \"PageDown\", \"keyCode\": 34},\n    # Function keys\n    \"f1\": {\"key\": \"F1\", \"code\": \"F1\", \"keyCode\": 112},\n    \"f2\": {\"key\": \"F2\", \"code\": \"F2\", \"keyCode\": 113},\n    \"f3\": {\"key\": \"F3\", \"code\": \"F3\", \"keyCode\": 114},\n    \"f4\": {\"key\": \"F4\", \"code\": \"F4\", \"keyCode\": 115},\n    \"f5\": {\"key\": \"F5\", \"code\": \"F5\", \"keyCode\": 116},\n    \"f6\": {\"key\": \"F6\", \"code\": \"F6\", \"keyCode\": 117},\n    \"f7\": {\"key\": \"F7\", \"code\": \"F7\", \"keyCode\": 118},\n    \"f8\": {\"key\": \"F8\", \"code\": \"F8\", \"keyCode\": 119},\n    \"f9\": {\"key\": \"F9\", \"code\": \"F9\", \"keyCode\": 120},\n    \"f10\": {\"key\": \"F10\", \"code\": \"F10\", \"keyCode\": 121},\n    \"f11\": {\"key\": \"F11\", \"code\": \"F11\", \"keyCode\": 122},\n    \"f12\": {\"key\": \"F12\", \"code\": \"F12\", \"keyCode\": 123},\n    # Special characters\n    \";\": {\"key\": \";\", \"code\": \"Semicolon\", \"keyCode\": 186, \"text\": \";\"},\n    \"=\": {\"key\": \"=\", \"code\": \"Equal\", \"keyCode\": 187, \"text\": \"=\"},\n    \",\": {\"key\": \",\", \"code\": \"Comma\", \"keyCode\": 188, \"text\": \",\"},\n    \"-\": {\"key\": \"-\", \"code\": \"Minus\", \"keyCode\": 189, \"text\": \"-\"},\n    \".\": {\"key\": \".\", \"code\": \"Period\", \"keyCode\": 190, \"text\": \".\"},\n    \"/\": {\"key\": \"/\", \"code\": \"Slash\", \"keyCode\": 191, \"text\": \"/\"},\n    \"`\": {\"key\": \"`\", \"code\": \"Backquote\", \"keyCode\": 192, \"text\": \"`\"},\n    \"[\": {\"key\": \"[\", \"code\": \"BracketLeft\", \"keyCode\": 219, \"text\": \"[\"},\n    \"\\\\\": {\"key\": \"\\\\\", \"code\": \"Backslash\", \"keyCode\": 220, \"text\": \"\\\\\"},\n    \"]\": {\"key\": \"]\", \"code\": \"BracketRight\", \"keyCode\": 221, \"text\": \"]\"},\n    \"'\": {\"key\": \"'\", \"code\": \"Quote\", \"keyCode\": 222, \"text\": \"'\"},\n    \"!\": {\"key\": \"!\", \"code\": \"Digit1\", \"keyCode\": 49, \"text\": \"!\"},\n    \"@\": {\"key\": \"@\", \"code\": \"Digit2\", \"keyCode\": 50, \"text\": \"@\"},\n    \"#\": {\"key\": \"#\", \"code\": \"Digit3\", \"keyCode\": 51, \"text\": \"#\"},\n    \"$\": {\"key\": \"$\", \"code\": \"Digit4\", \"keyCode\": 52, \"text\": \"$\"},\n    \"%\": {\"key\": \"%\", \"code\": \"Digit5\", \"keyCode\": 53, \"text\": \"%\"},\n    \"^\": {\"key\": \"^\", \"code\": \"Digit6\", \"keyCode\": 54, \"text\": \"^\"},\n    \"&\": {\"key\": \"&\", \"code\": \"Digit7\", \"keyCode\": 55, \"text\": \"&\"},\n    \"*\": {\"key\": \"*\", \"code\": \"Digit8\", \"keyCode\": 56, \"text\": \"*\"},\n    \"(\": {\"key\": \"(\", \"code\": \"Digit9\", \"keyCode\": 57, \"text\": \"(\"},\n    \")\": {\"key\": \")\", \"code\": \"Digit0\", \"keyCode\": 48, \"text\": \")\"},\n    \"_\": {\"key\": \"_\", \"code\": \"Minus\", \"keyCode\": 189, \"text\": \"_\"},\n    \"+\": {\"key\": \"+\", \"code\": \"Equal\", \"keyCode\": 187, \"text\": \"+\"},\n    \"{\": {\"key\": \"{\", \"code\": \"BracketLeft\", \"keyCode\": 219, \"text\": \"{\"},\n    \"}\": {\"key\": \"}\", \"code\": \"BracketRight\", \"keyCode\": 221, \"text\": \"}\"},\n    \"|\": {\"key\": \"|\", \"code\": \"Backslash\", \"keyCode\": 220, \"text\": \"|\"},\n    \":\": {\"key\": \":\", \"code\": \"Semicolon\", \"keyCode\": 186, \"text\": \":\"},\n    '\"': {\"key\": '\"', \"code\": \"Quote\", \"keyCode\": 222, \"text\": '\"'},\n    \"<\": {\"key\": \"<\", \"code\": \"Comma\", \"keyCode\": 188, \"text\": \"<\"},\n    \">\": {\"key\": \">\", \"code\": \"Period\", \"keyCode\": 190, \"text\": \">\"},\n    \"?\": {\"key\": \"?\", \"code\": \"Slash\", \"keyCode\": 191, \"text\": \"?\"},\n    \"~\": {\"key\": \"~\", \"code\": \"Backquote\", \"keyCode\": 192, \"text\": \"~\"},\n    # Lock keys\n    \"capslock\": {\"key\": \"CapsLock\", \"code\": \"CapsLock\", \"keyCode\": 20},\n    \"numlock\": {\"key\": \"NumLock\", \"code\": \"NumLock\", \"keyCode\": 144},\n    \"scrolllock\": {\"key\": \"ScrollLock\", \"code\": \"ScrollLock\", \"keyCode\": 145},\n    # Media keys\n    \"pause\": {\"key\": \"Pause\", \"code\": \"Pause\", \"keyCode\": 19},\n    \"insert\": {\"key\": \"Insert\", \"code\": \"Insert\", \"keyCode\": 45},\n    \"printscreen\": {\"key\": \"PrintScreen\", \"code\": \"PrintScreen\", \"keyCode\": 44},\n    # Numpad\n    \"numpad0\": {\n        \"key\": \"0\",\n        \"code\": \"Numpad0\",\n        \"keyCode\": 96,\n        \"isKeypad\": True,\n    },\n    \"numpad1\": {\n        \"key\": \"1\",\n        \"code\": \"Numpad1\",\n        \"keyCode\": 97,\n        \"isKeypad\": True,\n    },\n    \"numpad2\": {\n        \"key\": \"2\",\n        \"code\": \"Numpad2\",\n        \"keyCode\": 98,\n        \"isKeypad\": True,\n    },\n    \"numpad3\": {\n        \"key\": \"3\",\n        \"code\": \"Numpad3\",\n        \"keyCode\": 99,\n        \"isKeypad\": True,\n    },\n    \"numpad4\": {\n        \"key\": \"4\",\n        \"code\": \"Numpad4\",\n        \"keyCode\": 100,\n        \"isKeypad\": True,\n    },\n    \"numpad5\": {\n        \"key\": \"5\",\n        \"code\": \"Numpad5\",\n        \"keyCode\": 101,\n        \"isKeypad\": True,\n    },\n    \"numpad6\": {\n        \"key\": \"6\",\n        \"code\": \"Numpad6\",\n        \"keyCode\": 102,\n        \"isKeypad\": True,\n    },\n    \"numpad7\": {\n        \"key\": \"7\",\n        \"code\": \"Numpad7\",\n        \"keyCode\": 103,\n        \"isKeypad\": True,\n    },\n    \"numpad8\": {\n        \"key\": \"8\",\n        \"code\": \"Numpad8\",\n        \"keyCode\": 104,\n        \"isKeypad\": True,\n    },\n    \"numpad9\": {\n        \"key\": \"9\",\n        \"code\": \"Numpad9\",\n        \"keyCode\": 105,\n        \"isKeypad\": True,\n    },\n    \"numpadmultiply\": {\n        \"key\": \"*\",\n        \"code\": \"NumpadMultiply\",\n        \"keyCode\": 106,\n        \"isKeypad\": True,\n    },\n    \"numpadadd\": {\n        \"key\": \"+\",\n        \"code\": \"NumpadAdd\",\n        \"keyCode\": 107,\n        \"isKeypad\": True,\n    },\n    \"numpadsubtract\": {\n        \"key\": \"-\",\n        \"code\": \"NumpadSubtract\",\n        \"keyCode\": 109,\n        \"isKeypad\": True,\n    },\n    \"numpaddecimal\": {\n        \"key\": \".\",\n        \"code\": \"NumpadDecimal\",\n        \"keyCode\": 110,\n        \"isKeypad\": True,\n    },\n    \"numpaddivide\": {\n        \"key\": \"/\",\n        \"code\": \"NumpadDivide\",\n        \"keyCode\": 111,\n        \"isKeypad\": True,\n    },\n}\n"
  },
  {
    "path": "browser-use-demo/browser_use_demo/browser_tool_utils/browser_text_script.js",
    "content": "// Script to extract raw text content from the page, prioritizing article content\n\n(function() {\n    try {\n        // Priority order for finding article content\n        const selectors = [\n            'article',\n            'main',\n            '[class*=\"articleBody\"]',\n            '[class*=\"article-body\"]',\n            '[class*=\"post-content\"]',\n            '[class*=\"entry-content\"]',\n            '[class*=\"content-body\"]',\n            '[role=\"main\"]',\n            '.content',\n            '#content'\n        ];\n\n        let contentElement = null;\n\n        // Try each selector in order\n        for (const selector of selectors) {\n            const elements = document.querySelectorAll(selector);\n            if (elements.length > 0) {\n                // If multiple elements found, prefer the one with the most text content\n                let bestElement = elements[0];\n                let maxTextLength = 0;\n\n                elements.forEach((el) => {\n                    const textLength = el.textContent?.length || 0;\n                    if (textLength > maxTextLength) {\n                        maxTextLength = textLength;\n                        bestElement = el;\n                    }\n                });\n\n                contentElement = bestElement;\n                break;\n            }\n        }\n\n        if (!contentElement) {\n            // Fallback to body if no specific content element found\n            contentElement = document.body;\n        }\n\n        // Extract text content\n        const textContent = contentElement.textContent || '';\n\n        // Clean up the text: remove excessive whitespace, normalize line breaks\n        const cleanedText = textContent\n            .replace(/\\s+/g, ' ')           // Replace multiple spaces with single space\n            .replace(/\\n{3,}/g, '\\n\\n')     // Replace 3+ newlines with double newline\n            .trim();\n\n        return {\n            text: cleanedText,\n            source: contentElement.tagName.toLowerCase(),\n            title: document.title,\n            url: window.location.href\n        };\n    } catch (error) {\n        console.error('Error extracting page text:', error);\n        throw new Error('Error extracting page text: ' + (error.message || 'Unknown error'));\n    }\n})"
  },
  {
    "path": "browser-use-demo/browser_use_demo/display_constants.py",
    "content": "\"\"\"Display and browser configuration constants.\n\nThese values are hardcoded and not configurable via environment variables.\nThe standard resolution is 1920x1080 for consistent browser automation.\n\"\"\"\n\n# Display configuration\nDISPLAY_WIDTH = 1920\nDISPLAY_HEIGHT = 1080\nDISPLAY_NUM = 1\n\n# Browser viewport configuration (matches display for consistency)\nBROWSER_WIDTH = 1920\nBROWSER_HEIGHT = 1080"
  },
  {
    "path": "browser-use-demo/browser_use_demo/loop.py",
    "content": "\"\"\"\nSampling loop for browser automation with Claude\n\"\"\"\n\nimport os\nfrom collections.abc import Callable\nfrom datetime import datetime\nfrom enum import StrEnum\nfrom typing import Optional\n\nimport httpx\n\nfrom anthropic import (\n    Anthropic,\n    AnthropicBedrock,\n    AnthropicVertex,\n)\nfrom anthropic.types.beta import (\n    BetaCacheControlEphemeralParam,\n    BetaContentBlockParam,\n    BetaMessageParam,\n    BetaTextBlockParam,\n)\n\nfrom .message_handler import MessageBuilder, ResponseProcessor\nfrom .tools import BrowserTool, ToolCollection, ToolResult\n\nPROMPT_CACHING_BETA_FLAG = \"prompt-caching-2024-07-31\"\n\n\nclass APIProvider(StrEnum):\n    ANTHROPIC = \"anthropic\"\n    BEDROCK = \"bedrock\"\n    VERTEX = \"vertex\"\n\n\n# Browser-specific system prompt\nBROWSER_SYSTEM_PROMPT = f\"\"\"<SYSTEM_CAPABILITY>\n* You control a Chromium browser via Playwright automation.\n* The current date is {datetime.today().strftime(\"%A, %B %-d, %Y\")}.\n</SYSTEM_CAPABILITY>\n\n<TOOL_GUIDANCE>\nYou receive a screenshot at the start of each turn. Look at it to see the current page - if you're already where you need to be, don't re-navigate.\n\nAfter navigating to a new page, always call read_page to get element references (ref_1, ref_2, etc.) before interacting with the page. Use these refs with your interaction tools (click, type, hover, form_input, etc.). Refs are more reliable than coordinates.\n\nWhen you need to extract or read text content from a page, always use get_page_text - don't try to read text from screenshots.\n\nIf DOM-based actions (refs) aren't working, fall back to screenshot + coordinate-based actions.\n</TOOL_GUIDANCE>\n\n<TIPS>\n* Prefer get_page_text over scrolling when looking for information - it's faster and more reliable\n* Use execute_js to extract data from JavaScript variables, localStorage, or trigger behaviors not accessible through clicks\n* Use full URLs with https://\n* Use wait for slow-loading pages\n* Use scroll_to with a ref to reveal elements\n* Use form_input with refs for form fields\n* Use key for shortcuts (e.g., \"ctrl+a\")\n* Close popups when they appear\n* Verify actions succeeded before moving on\n</TIPS>\"\"\"\n\n\nasync def sampling_loop(\n    *,\n    model: str,\n    provider: APIProvider,\n    system_prompt_suffix: str,\n    messages: list[BetaMessageParam],\n    output_callback: Callable[[BetaContentBlockParam], None],\n    tool_output_callback: Callable[[ToolResult, str], None],\n    api_response_callback: Callable[\n        [httpx.Request | None, httpx.Response | object | None, Exception | None], None\n    ],\n    api_key: str,\n    only_n_most_recent_images: int | None = None,\n    max_tokens: int = 4096,\n    browser_tool: Optional[BrowserTool] = None,\n):\n    \"\"\"\n    Sampling loop for browser automation.\n\n    Args:\n        browser_tool: Optional persistent browser tool instance. If not provided, creates a new one.\n    \"\"\"\n    # Reuse existing browser tool or create a new one\n    if browser_tool is None:\n        # Create browser tool with standard dimensions\n        browser_tool = BrowserTool()\n\n    tool_collection = ToolCollection(browser_tool)\n\n    # Build system prompt\n    system = BetaTextBlockParam(\n        type=\"text\",\n        text=f\"{BROWSER_SYSTEM_PROMPT}{' ' + system_prompt_suffix if system_prompt_suffix else ''}\",\n    )\n\n    while True:\n        # Configure client and betas\n        betas = []\n        enable_prompt_caching = False\n\n        if provider == APIProvider.ANTHROPIC:\n            client = Anthropic(api_key=api_key, max_retries=4)\n            enable_prompt_caching = True\n        elif provider == APIProvider.VERTEX:\n            client = AnthropicVertex()\n        elif provider == APIProvider.BEDROCK:\n            client = AnthropicBedrock()\n        else:\n            raise ValueError(f\"Unsupported provider: {provider}\")\n\n        if enable_prompt_caching:\n            betas.append(PROMPT_CACHING_BETA_FLAG)\n            # Add cache control to system prompt\n            system = BetaTextBlockParam(\n                type=\"text\",\n                text=system[\"text\"],\n                cache_control=BetaCacheControlEphemeralParam(type=\"ephemeral\"),\n            )\n\n        # Make API call\n        try:\n            api_kwargs = {\n                \"max_tokens\": max_tokens,\n                \"messages\": messages,\n                \"model\": model,\n                \"system\": [system],\n                \"tools\": tool_collection.to_params(),\n            }\n            # Only include betas if there are any (e.g., prompt caching)\n            if betas:\n                api_kwargs[\"betas\"] = betas\n                response = client.beta.messages.create(**api_kwargs)\n            else:\n                # Use regular messages API when no beta features are needed\n                response = client.messages.create(**api_kwargs)\n        except Exception as e:\n            api_response_callback(None, None, e)\n            raise e\n\n        api_response_callback(None, response, None)\n\n        # Process response using our new abstractions\n        processor = ResponseProcessor()\n        processed = processor.process_response(response)\n\n        # Output all content blocks to callbacks\n        for content_block in processed.assistant_content:\n            output_callback(content_block)\n\n        # Build and append the complete assistant message (preserves text + tools)\n        builder = MessageBuilder()\n        builder.add_assistant_message(messages, processed.assistant_content)\n\n        # Execute tools and collect results if there are any tool uses\n        if processed.tool_uses:\n            tool_results = await processor.execute_tools(\n                processed.tool_uses,\n                tool_collection,\n                tool_output_callback\n            )\n\n            # Add all tool results as a single user message\n            builder.add_tool_results(messages, tool_results)\n\n            # Continue the loop to process any follow-up\n        else:\n            # No tools used, conversation can end here\n            return messages\n\n\ndef _maybe_filter_to_n_most_recent_images(\n    messages: list[BetaMessageParam],\n    images_to_keep: int,\n    min_removal_threshold: int = 10,\n):\n    \"\"\"\n    Filter messages to keep only the N most recent images.\n    \"\"\"\n    if images_to_keep <= 0:\n        raise ValueError(\"images_to_keep must be > 0\")\n\n    total_images = sum(\n        1\n        for message in messages\n        if message[\"role\"] == \"user\"\n        for block in message.get(\"content\", [])\n        if isinstance(block, dict) and block.get(\"type\") == \"image\"\n    )\n\n    images_to_remove = total_images - images_to_keep\n    if images_to_remove < min_removal_threshold:\n        return\n\n    images_removed = 0\n    for message in messages:\n        if message[\"role\"] == \"user\" and isinstance(message.get(\"content\"), list):\n            new_content = []\n            for block in message[\"content\"]:\n                if isinstance(block, dict) and block.get(\"type\") == \"image\":\n                    if images_removed < images_to_remove:\n                        images_removed += 1\n                        continue\n                new_content.append(block)\n            message[\"content\"] = new_content\n"
  },
  {
    "path": "browser-use-demo/browser_use_demo/message_handler.py",
    "content": "\"\"\"\nMessage handling abstractions for proper API response processing.\n\nThis module provides clean abstractions for processing API responses and building\nmessages that preserve both text explanations and tool uses together, matching\nthe Chrome extension's behavior.\n\"\"\"\n\nfrom collections.abc import Callable\nfrom dataclasses import dataclass\nfrom typing import Any, Optional, cast\n\nfrom anthropic.types.beta import (\n    BetaContentBlockParam,\n    BetaImageBlockParam,\n    BetaMessageParam,\n    BetaTextBlockParam,\n    BetaToolResultBlockParam,\n)\n\nfrom .tools import ToolCollection, ToolResult\n\n\n@dataclass\nclass ProcessedResponse:\n    \"\"\"Container for processed API response data.\"\"\"\n\n    assistant_content: list[BetaContentBlockParam]\n    tool_uses: list[dict[str, Any]]\n    has_text: bool\n    has_tools: bool\n\n\nclass ResponseProcessor:\n    \"\"\"\n    Handles API response processing with proper message preservation.\n\n    This class ensures that text explanations are preserved alongside tool uses,\n    matching the behavior of the Chrome extension.\n    \"\"\"\n\n    def process_response(self, response) -> ProcessedResponse:\n        \"\"\"\n        Process an API response into a structured format.\n\n        Args:\n            response: The API response from Anthropic\n\n        Returns:\n            ProcessedResponse containing all content blocks and metadata\n        \"\"\"\n        assistant_content = []\n        tool_uses = []\n        has_text = False\n        has_tools = False\n\n        for content_block in response.content:\n            if content_block.type == \"text\":\n                has_text = True\n                assistant_content.append({\n                    \"type\": \"text\",\n                    \"text\": content_block.text\n                })\n            elif content_block.type == \"tool_use\":\n                has_tools = True\n                tool_use_dict = {\n                    \"type\": \"tool_use\",\n                    \"id\": content_block.id,\n                    \"name\": content_block.name,\n                    \"input\": content_block.input\n                }\n                assistant_content.append(tool_use_dict)\n                tool_uses.append(tool_use_dict)\n\n        return ProcessedResponse(\n            assistant_content=assistant_content,\n            tool_uses=tool_uses,\n            has_text=has_text,\n            has_tools=has_tools\n        )\n\n    async def execute_tools(\n        self,\n        tool_uses: list[dict[str, Any]],\n        tool_collection: ToolCollection,\n        tool_output_callback: Optional[Callable[[ToolResult, str], None]] = None\n    ) -> list[BetaToolResultBlockParam]:\n        \"\"\"\n        Execute tools and collect results.\n\n        Args:\n            tool_uses: List of tool use blocks to execute\n            tool_collection: The tool collection for execution\n            tool_output_callback: Optional callback for tool results\n\n        Returns:\n            List of tool result blocks\n        \"\"\"\n        tool_results = []\n\n        for tool_use in tool_uses:\n            tool_id = tool_use[\"id\"]\n            tool_name = tool_use[\"name\"]\n            tool_input = tool_use[\"input\"]\n\n            try:\n                tool = tool_collection.tool_map.get(tool_name)\n                if not tool:\n                    raise ValueError(f\"Unknown tool: {tool_name}\")\n\n                result = await tool(**tool_input)\n\n                if tool_output_callback:\n                    tool_output_callback(result, tool_id)\n\n                tool_result = self._build_tool_result(result, tool_id)\n                tool_results.append(tool_result)\n\n            except Exception as e:\n                error_result = BetaToolResultBlockParam(\n                    type=\"tool_result\",\n                    tool_use_id=tool_id,\n                    is_error=True,\n                    content=[{\"type\": \"text\", \"text\": str(e)}]\n                )\n                tool_results.append(error_result)\n\n                if tool_output_callback:\n                    error_tool_result = ToolResult(error=str(e))\n                    tool_output_callback(error_tool_result, tool_id)\n\n        return tool_results\n\n    def _build_tool_result(\n        self,\n        result: ToolResult,\n        tool_use_id: str\n    ) -> BetaToolResultBlockParam:\n        \"\"\"\n        Build a tool result block from a ToolResult.\n\n        Args:\n            result: The tool execution result\n            tool_use_id: The ID of the tool use\n\n        Returns:\n            A properly formatted tool result block\n        \"\"\"\n        tool_result = BetaToolResultBlockParam(\n            type=\"tool_result\",\n            tool_use_id=tool_use_id,\n            content=[]\n        )\n\n        content_list = cast(list[BetaTextBlockParam | BetaImageBlockParam], tool_result.get(\"content\", []))\n\n        if result.output:\n            output_text = result.output\n            if \"__PAGE_EXTRACTED__\" in output_text or \"__TEXT_EXTRACTED__\" in output_text:\n                if \"__FULL_CONTENT__\" in output_text:\n                    marker_pos = output_text.index(\"__FULL_CONTENT__\")\n                    output_text = output_text[marker_pos + len(\"__FULL_CONTENT__\") + 1:]\n\n            content_list.append({\n                \"type\": \"text\",\n                \"text\": output_text\n            })\n\n        if result.base64_image:\n            content_list.append({\n                \"type\": \"image\",\n                \"source\": {\n                    \"type\": \"base64\",\n                    \"media_type\": \"image/png\",\n                    \"data\": result.base64_image,\n                }\n            })\n\n        if result.error:\n            tool_result[\"is_error\"] = True\n            content_list.append({\n                \"type\": \"text\",\n                \"text\": f\"Error: {result.error}\"\n            })\n\n        return tool_result\n\n\nclass MessageBuilder:\n    \"\"\"\n    Builds properly structured messages for the API.\n\n    This class ensures messages are constructed in a way that preserves\n    all content, matching the Chrome extension's behavior.\n    \"\"\"\n\n    def add_assistant_message(\n        self,\n        messages: list[BetaMessageParam],\n        content: list[BetaContentBlockParam]\n    ) -> None:\n        \"\"\"\n        Add a complete assistant message with all content blocks.\n\n        Args:\n            messages: The message list to append to\n            content: The content blocks for the assistant message\n        \"\"\"\n        if content:  # Only add if there's content\n            messages.append({\n                \"role\": \"assistant\",\n                \"content\": content\n            })\n\n    def add_tool_results(\n        self,\n        messages: list[BetaMessageParam],\n        tool_results: list[BetaToolResultBlockParam]\n    ) -> None:\n        \"\"\"\n        Add tool results as a user message.\n\n        Args:\n            messages: The message list to append to\n            tool_results: The tool result blocks to add\n        \"\"\"\n        if tool_results:  # Only add if there are results\n            messages.append({\n                \"role\": \"user\",\n                \"content\": tool_results\n            })\n\n    def ensure_message_integrity(\n        self,\n        messages: list[BetaMessageParam]\n    ) -> bool:\n        \"\"\"\n        Validate that messages maintain proper structure.\n\n        Args:\n            messages: The message list to validate\n\n        Returns:\n            True if messages are properly structured, False otherwise\n        \"\"\"\n        if not messages:\n            return True\n\n        for message in messages:\n            role = message.get(\"role\")\n            if not role:\n                return False\n\n            if \"content\" not in message:\n                return False\n\n            content = message[\"content\"]\n            if isinstance(content, list) and len(content) == 0:\n                return False\n\n        return True\n\n    def extract_text_from_message(\n        self,\n        message: BetaMessageParam\n    ) -> Optional[str]:\n        \"\"\"\n        Extract text content from a message.\n\n        Args:\n            message: The message to extract text from\n\n        Returns:\n            The extracted text or None if no text content\n        \"\"\"\n        if message.get(\"role\") != \"assistant\":\n            return None\n\n        content = message.get(\"content\", [])\n        if isinstance(content, str):\n            return content\n\n        text_parts = []\n        for block in content:\n            if isinstance(block, dict) and block.get(\"type\") == \"text\":\n                text_parts.append(block.get(\"text\", \"\"))\n\n        return \" \".join(text_parts) if text_parts else None\n"
  },
  {
    "path": "browser-use-demo/browser_use_demo/message_renderer.py",
    "content": "\"\"\"\nMessage rendering functionality for the Browser Use Demo.\n\nThis module handles all message rendering logic for the Streamlit interface,\nseparating presentation concerns from the main application logic.\n\"\"\"\n\nimport base64\nfrom typing import cast\n\nimport streamlit as st\nfrom anthropic.types.beta import BetaContentBlockParam\n\nfrom browser_use_demo.tools import ToolResult\nfrom browser_use_demo.tools.coordinate_scaling import CoordinateScaler\n\n\nclass Sender:\n    \"\"\"Message sender types.\"\"\"\n\n    USER = \"user\"\n    BOT = \"assistant\"\n    TOOL = \"tool\"\n\n\nclass MessageRenderer:\n    \"\"\"Handles rendering of messages in the Streamlit chat interface.\"\"\"\n\n    def __init__(self, session_state):\n        \"\"\"Initialize the MessageRenderer with session state access.\n\n        Args:\n            session_state: Streamlit session state object for accessing configuration\n        \"\"\"\n        self.session_state = session_state\n\n    def _scale_browser_coordinates(self, input_dict: dict) -> dict:\n        \"\"\"Apply coordinate scaling to browser tool inputs for display.\n\n        This ensures the displayed coordinates match what the browser tool will actually use.\n        Uses the CoordinateScaler for consistent scaling across the codebase.\n\n        Args:\n            input_dict: The tool input dictionary\n\n        Returns:\n            Modified input dict with scaled coordinates\n        \"\"\"\n        # Only process browser tool inputs with coordinates\n        if not isinstance(input_dict, dict):\n            return input_dict\n\n        # Get browser tool dimensions if available\n        browser_tool = getattr(self.session_state, 'browser_tool', None)\n        if not browser_tool:\n            return input_dict\n\n        # Clone the input to avoid modifying the original\n        import copy\n        scaled_input = copy.deepcopy(input_dict)\n\n        # Get viewport dimensions\n        width = browser_tool.width\n        height = browser_tool.height\n\n        # Scale various coordinate fields using CoordinateScaler\n        if 'coordinate' in scaled_input:\n            scaled_input['coordinate'] = CoordinateScaler.scale_coordinate_list(\n                scaled_input['coordinate'], width, height\n            )\n\n        if 'start_coordinate' in scaled_input:\n            scaled_input['start_coordinate'] = CoordinateScaler.scale_coordinate_list(\n                scaled_input['start_coordinate'], width, height\n            )\n\n        return scaled_input\n\n    def render(self, sender: str, message: str | BetaContentBlockParam | ToolResult):\n        \"\"\"Render a message in the chat interface.\n\n        Args:\n            sender: The sender type (USER, BOT, or TOOL)\n            message: The message content to render\n        \"\"\"\n        # Early return for empty messages or hidden screenshots without content\n        if self._should_skip_message(message):\n            return\n\n        with st.chat_message(sender):\n            self._render_message_content(message)\n\n    def _should_skip_message(self, message) -> bool:\n        \"\"\"Check if message should be skipped from rendering.\n\n        Args:\n            message: The message to check\n\n        Returns:\n            True if the message should be skipped, False otherwise\n        \"\"\"\n        if not message:\n            return True\n\n        # Skip tool results that only have screenshots when screenshots are hidden\n        is_tool_result = not isinstance(message, str | dict)\n        if is_tool_result and self.session_state.hide_screenshots:\n            return not hasattr(message, \"error\") and not hasattr(message, \"output\")\n\n        return False\n\n    def _render_message_content(self, message):\n        \"\"\"Render the actual message content based on its type.\n\n        Args:\n            message: The message content to render\n        \"\"\"\n        # Define rendering strategies for different message types\n        renderers = {\n            \"tool_result\": self._render_tool_result,\n            \"dict\": self._render_dict_message,\n            \"string\": lambda msg: st.markdown(msg),\n        }\n\n        # Determine message type and render accordingly\n        if not isinstance(message, str | dict):\n            # It's a ToolResult object\n            renderers[\"tool_result\"](cast(ToolResult, message))\n        elif isinstance(message, dict):\n            renderers[\"dict\"](message)\n        else:\n            renderers[\"string\"](message)\n\n    def _render_tool_result(self, tool_result: ToolResult):\n        \"\"\"Render a tool result with output, error, and optional image.\n\n        Args:\n            tool_result: The ToolResult object to render\n        \"\"\"\n        if tool_result.output:\n            # Check if this is a text extraction result with special markers\n            if \"__PAGE_EXTRACTED__\" in tool_result.output or \"__TEXT_EXTRACTED__\" in tool_result.output:\n                # Extract just the summary for display\n                lines = tool_result.output.split(\"\\n\")\n                summary_lines = []\n                in_summary = False\n\n                for line in lines:\n                    if \"__PAGE_EXTRACTED__\" in line or \"__TEXT_EXTRACTED__\" in line:\n                        in_summary = True\n                        continue\n                    if \"__FULL_CONTENT__\" in line:\n                        break\n                    if in_summary:\n                        summary_lines.append(line)\n\n                # Display only the summary\n                if summary_lines:\n                    st.markdown(\"\\n\".join(summary_lines))\n            else:\n                # Regular tool output\n                st.markdown(tool_result.output)\n\n        if tool_result.error:\n            st.error(tool_result.error)\n        if tool_result.base64_image and not self.session_state.hide_screenshots:\n            st.image(base64.b64decode(tool_result.base64_image))\n\n    def _render_dict_message(self, message: dict):\n        \"\"\"Render dictionary-based messages based on their type field.\n\n        Args:\n            message: Dictionary containing the message to render\n        \"\"\"\n        message_type = message.get(\"type\", \"\")\n\n        # Dispatch table for different message types\n        type_handlers = {\n            \"text\": lambda: st.write(message[\"text\"]),\n            \"tool_use\": lambda: self._render_tool_use(message),\n            \"tool_result\": lambda: self._render_stored_tool_result(message),\n        }\n\n        # Execute the appropriate handler or fall back to generic display\n        handler = type_handlers.get(message_type, lambda: st.write(message))\n        handler()\n\n    def _render_tool_use(self, message: dict):\n        \"\"\"Render a tool use message with coordinate scaling for browser tools.\n\n        Args:\n            message: Dictionary containing tool use information\n        \"\"\"\n        tool_name = message.get('name', 'unknown')\n        tool_input = message.get('input', {})\n\n        # Apply coordinate scaling for browser tool\n        if tool_name == 'browser':\n            tool_input = self._scale_browser_coordinates(tool_input)\n\n        st.code(f\"Tool Use: {tool_name}\\nInput: {tool_input}\")\n\n    def _render_stored_tool_result(self, message: dict):\n        \"\"\"Render a tool result that was stored in session state.\n\n        Args:\n            message: Dictionary containing the tool_use_id reference\n        \"\"\"\n        tool_id = message.get(\"tool_use_id\")\n        if tool_id and tool_id in self.session_state.tools:\n            self._render_tool_result(self.session_state.tools[tool_id])\n\n    def render_conversation_history(self, messages: list):\n        \"\"\"Render all messages in conversation history.\n\n        This method processes a list of messages and renders each one\n        according to its role and content type, eliminating deep nesting.\n\n        Args:\n            messages: List of message dictionaries from session state\n        \"\"\"\n        for message in messages:\n            self._render_message_by_role(message)\n\n    def _render_message_by_role(self, message: dict):\n        \"\"\"Route message rendering based on role.\n\n        Args:\n            message: Message dictionary containing role and content\n        \"\"\"\n        role_handlers = {\n            \"user\": lambda m: self._render_user_content(m[\"content\"]),\n            \"assistant\": lambda m: self._render_assistant_content(m[\"content\"]),\n        }\n\n        handler = role_handlers.get(message[\"role\"])\n        if handler:\n            handler(message)\n\n    def _render_user_content(self, content):\n        \"\"\"Render user message content.\n\n        Handles both single items and lists of content blocks,\n        skipping image blocks in conversation history.\n\n        Args:\n            content: User message content (string, dict, or list)\n        \"\"\"\n        for item in self._normalize_content(content):\n            # Skip image blocks in history\n            if isinstance(item, dict) and item.get(\"type\") == \"image\":\n                continue\n\n            # Extract text from dict blocks or use item directly\n            if isinstance(item, dict):\n                if item.get(\"type\") == \"text\":\n                    text_content = item.get(\"text\", \"\")\n                    self.render(Sender.USER, text_content)\n                else:\n                    # For other dict types, cast as BetaContentBlockParam\n                    self.render(Sender.USER, cast(BetaContentBlockParam, item))\n            else:\n                self.render(Sender.USER, item)\n\n    def _render_assistant_content(self, content):\n        \"\"\"Render assistant message content.\n\n        Handles both single items and lists of content blocks,\n        properly routing tool results to the TOOL sender.\n\n        Args:\n            content: Assistant message content (string, dict, or list)\n        \"\"\"\n        for item in self._normalize_content(content):\n            if isinstance(item, dict) and item.get(\"type\") == \"tool_result\":\n                # Handle tool results by fetching from session state\n                tool_id = item.get(\"tool_use_id\")\n                if tool_id and tool_id in self.session_state.tools:\n                    self.render(Sender.TOOL, self.session_state.tools[tool_id])\n            elif isinstance(item, dict):\n                # Cast dict items as BetaContentBlockParam\n                self.render(Sender.BOT, cast(BetaContentBlockParam, item))\n            else:\n                # String or other types\n                self.render(Sender.BOT, item)\n\n    def _normalize_content(self, content):\n        \"\"\"Convert content to list for uniform processing.\n\n        This eliminates duplicate code for handling list vs non-list content.\n\n        Args:\n            content: Content that may be a single item or list\n\n        Returns:\n            List of content items for processing\n        \"\"\"\n        return content if isinstance(content, list) else [content]\n"
  },
  {
    "path": "browser-use-demo/browser_use_demo/requirements.txt",
    "content": "streamlit==1.41.0\nanthropic[bedrock,vertex]>=0.39.0\njsonschema==4.22.0\nboto3>=1.28.57\ngoogle-auth<3,>=2\nplaywright>=1.40.0"
  },
  {
    "path": "browser-use-demo/browser_use_demo/streamlit.py",
    "content": "\"\"\"\nBrowser Use Demo - Streamlit interface for browser automation with Claude\n\"\"\"\n\nimport asyncio\nimport base64\nimport io\nimport json\nimport os\nimport time\nimport traceback\nimport zipfile\nfrom datetime import datetime\nfrom pathlib import PosixPath\n\nimport streamlit as st\nfrom anthropic.types.beta import BetaContentBlockParam\n\nfrom anthropic import RateLimitError\nfrom browser_use_demo.loop import APIProvider, sampling_loop\nfrom browser_use_demo.message_renderer import MessageRenderer, Sender\nfrom browser_use_demo.tools import ToolResult\n\nPROVIDER_TO_DEFAULT_MODEL_NAME: dict[APIProvider, str] = {\n    APIProvider.ANTHROPIC: \"claude-sonnet-4-5-20250929\",\n    APIProvider.BEDROCK: \"anthropic.claude-sonnet-4-5-20250929-v1:0\",\n    APIProvider.VERTEX: \"claude-sonnet-4-5@20250929\",\n}\n\nCONFIG_DIR = PosixPath(\"~/.anthropic\").expanduser()\nAPI_KEY_FILE = CONFIG_DIR / \"api_key\"\n\nSTREAMLIT_STYLE = \"\"\"\n<style>\n    /* Hide the streamlit deploy button */\n    .stDeployButton {\n        visibility: hidden;\n    }\n    section[data-testid=\"stSidebar\"] {\n        width: 360px !important;\n    }\n    /* Make the chat input stick to the bottom */\n    .stChatInputContainer {\n        position: sticky;\n        bottom: 0;\n        background: white;\n        z-index: 999;\n    }\n</style>\n\"\"\"\n\n# Claude 4.5 models for browser automation\nBROWSER_COMPATIBLE_MODELS = [\n    \"claude-sonnet-4-5-20250929\",\n    \"claude-opus-4-5-20251101\",\n    \"claude-haiku-4-5-20251001\",\n]\n\n\ndef setup_state():\n    \"\"\"Initialize session state variables.\"\"\"\n    # Import here to avoid circular imports when browser_tool lambda is evaluated\n    from browser_use_demo.tools import BrowserTool\n\n    # Define all defaults in one place - use lambdas for lazy evaluation of complex values\n    defaults = {\n        # UI State\n        \"messages\": [],\n        \"system_prompt\": \"\",\n        \"hide_screenshots\": False,\n        \"rendered_message_count\": 0,  # Track rendered messages to avoid re-rendering\n        \"last_error\": None,  # Store last error message to display persistently\n        # API Configuration\n        \"api_key\": os.environ.get(\"ANTHROPIC_API_KEY\", \"\"),\n        \"provider\": APIProvider.ANTHROPIC,\n        \"max_tokens\": 8192,\n        \"model\": lambda: PROVIDER_TO_DEFAULT_MODEL_NAME[st.session_state.provider],\n        # Runtime State\n        \"tools\": {},\n        \"event_loop\": None,  # Persistent event loop for async operations\n        \"chat_disabled\": False,  # Simple flag to disable chat input\n        \"active_messages\": [],  # Store messages for current interaction\n        \"active_response_container\": None,  # Container reference for streaming responses\n        # Complex initialization - browser tool (inline lambda)\n        \"browser_tool\": lambda: BrowserTool(),\n    }\n\n    # Apply all defaults - evaluate lambdas when needed\n    for key, default_value in defaults.items():\n        if key not in st.session_state:\n            # If it's a callable (lambda), call it to get the actual value\n            if callable(default_value):\n                st.session_state[key] = default_value()\n            else:\n                st.session_state[key] = default_value\n\n\ndef _clean_text_extraction_markers(text: str) -> str:\n    \"\"\"Remove text extraction markers and return a summary.\"\"\"\n    if \"__PAGE_EXTRACTED__\" not in text and \"__TEXT_EXTRACTED__\" not in text:\n        return text\n\n    lines = text.split(\"\\n\")\n    summary = []\n    for line in lines:\n        if \"__FULL_CONTENT__\" in line:\n            break\n        if \"__PAGE_EXTRACTED__\" not in line and \"__TEXT_EXTRACTED__\" not in line:\n            summary.append(line)\n    return \"\\n\".join(summary) + \"\\n[Full content extracted but truncated for readability]\"\n\n\ndef create_transcript_zip(messages: list, include_images: bool = False) -> bytes:\n    \"\"\"Create a ZIP archive containing the transcript and optionally images.\n\n    Args:\n        messages: List of message dictionaries from session state\n        include_images: Whether to include images as separate files\n\n    Returns:\n        Bytes of the ZIP archive\n    \"\"\"\n    # Create an in-memory ZIP file\n    zip_buffer = io.BytesIO()\n\n    with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:\n        if include_images:\n            # Extract images and create transcript with file references\n            transcript_json, image_files = extract_images_from_messages(messages)\n\n            # Add images to ZIP\n            for idx, img_data in enumerate(image_files):\n                filename = f\"images/screenshot_{idx+1:04d}.png\"\n                try:\n                    img_bytes = base64.b64decode(img_data)\n                    zip_file.writestr(filename, img_bytes)\n                except Exception as e:\n                    print(f\"Error adding image to ZIP: {e}\")\n\n            # Add README\n            readme_content = f\"\"\"Browser Use Demo - Conversation Transcript\nGenerated: {datetime.now().isoformat()}\n\nThis archive contains:\n- transcript.json: The conversation transcript\n- images/: {len(image_files)} screenshot images referenced in the transcript\n\nThe transcript is in JSON format with images stored as separate PNG files.\nImage references in the transcript point to files in the images/ directory.\n\"\"\"\n            zip_file.writestr(\"README.txt\", readme_content)\n        else:\n            # Just create transcript without images\n            transcript_json = format_transcript_for_download(messages, False)\n\n            readme_content = f\"\"\"Browser Use Demo - Conversation Transcript\nGenerated: {datetime.now().isoformat()}\n\nThis archive contains:\n- transcript.json: The conversation transcript (text only)\n\nThe transcript is in JSON format and includes all text messages from the conversation.\n\"\"\"\n            zip_file.writestr(\"README.txt\", readme_content)\n\n        # Add the transcript JSON to the ZIP\n        zip_file.writestr(\"transcript.json\", transcript_json)\n\n    # Get the ZIP file bytes\n    zip_buffer.seek(0)\n    return zip_buffer.read()\n\n\nclass ImageExtractor:\n    \"\"\"Helper class to extract images and track their file references.\"\"\"\n\n    def __init__(self):\n        self.image_files = []\n        self.image_counter = 0\n\n    def extract_image(self, source: dict) -> dict:\n        \"\"\"Extract an image and return a file reference.\"\"\"\n        if source.get(\"type\") == \"base64\":\n            self.image_counter += 1\n            self.image_files.append(source.get(\"data\", \"\"))\n            return {\n                \"type\": \"image\",\n                \"file\": f\"images/screenshot_{self.image_counter:04d}.png\"\n            }\n        else:\n            return {\"type\": \"image\", \"note\": \"No image data\"}\n\n    def process_image_content(self, item: dict) -> dict:\n        \"\"\"Process image content type.\"\"\"\n        source = item.get(\"source\", {})\n        return self.extract_image(source)\n\n    def process_text_content(self, item: dict) -> dict:\n        \"\"\"Process text content type.\"\"\"\n        return {\n            \"type\": \"text\",\n            \"text\": _clean_text_extraction_markers(item.get(\"text\", \"\"))\n        }\n\n    def process_tool_use_content(self, item: dict) -> dict:\n        \"\"\"Process tool use content type.\"\"\"\n        return {\n            \"type\": \"tool_use\",\n            \"name\": item.get(\"name\", \"\"),\n            \"input\": item.get(\"input\", {})\n        }\n\n    def process_tool_result_content(self, item: dict) -> dict:\n        \"\"\"Process tool result content type.\"\"\"\n        tool_content = []\n        for content_item in item.get(\"content\", []):\n            if isinstance(content_item, dict):\n                content_type = content_item.get(\"type\")\n                if content_type == \"image\":\n                    source = content_item.get(\"source\", {})\n                    tool_content.append(self.extract_image(source))\n                elif content_type == \"text\":\n                    tool_content.append(self.process_text_content(content_item))\n                else:\n                    tool_content.append(content_item)\n\n        return {\n            \"type\": \"tool_result\",\n            \"tool_use_id\": item.get(\"tool_use_id\", \"\"),\n            \"content\": tool_content\n        }\n\n    def process_default_content(self, item: dict) -> dict:\n        \"\"\"Default processor for unknown content types.\"\"\"\n        return _format_content_item(item, False)\n\n\ndef extract_images_from_messages(messages: list) -> tuple:\n    \"\"\"Extract images from messages and create transcript with file references.\n\n    Returns:\n        Tuple of (transcript_json, list_of_base64_image_data)\n    \"\"\"\n    extractor = ImageExtractor()\n\n    # Content type processors\n    processors = {\n        \"image\": extractor.process_image_content,\n        \"text\": extractor.process_text_content,\n        \"tool_use\": extractor.process_tool_use_content,\n        \"tool_result\": extractor.process_tool_result_content,\n    }\n\n    def process_content(content):\n        \"\"\"Process content using appropriate processors.\"\"\"\n        if isinstance(content, str):\n            return content\n        elif isinstance(content, list):\n            processed = []\n            for item in content:\n                if isinstance(item, dict):\n                    content_type = item.get(\"type\")\n                    processor = processors.get(content_type, extractor.process_default_content)\n                    processed.append(processor(item))\n                else:\n                    processed.append(str(item))\n            return processed\n        else:\n            return str(content)\n\n    # Build transcript\n    transcript = {\n        \"timestamp\": datetime.now().isoformat(),\n        \"format_version\": \"2.0\",\n        \"image_storage\": \"separate_files\",\n        \"conversation\": []\n    }\n\n    # Process all messages\n    for message in messages:\n        cleaned_message = {\n            \"role\": message.get(\"role\"),\n            \"timestamp\": datetime.now().isoformat(),\n            \"content\": process_content(message.get(\"content\", \"\"))\n        }\n        transcript[\"conversation\"].append(cleaned_message)\n\n    return json.dumps(transcript, indent=2, ensure_ascii=False), extractor.image_files\n\n\ndef format_transcript_for_download(messages: list, include_images: bool = False) -> str:\n    \"\"\"Format conversation messages into a readable transcript.\n\n    Args:\n        messages: List of message dictionaries from session state\n        include_images: Whether to include base64 image data in the transcript\n\n    Returns:\n        Formatted JSON string of the conversation\n    \"\"\"\n    transcript = {\n        \"timestamp\": datetime.now().isoformat(),\n        \"format_version\": \"1.0\",\n        \"includes_images\": include_images,\n        \"conversation\": []\n    }\n\n    for message in messages:\n        cleaned_message = {\n            \"role\": message.get(\"role\"),\n            \"timestamp\": datetime.now().isoformat(),\n            \"content\": _format_message_content(message.get(\"content\", \"\"), include_images)\n        }\n        transcript[\"conversation\"].append(cleaned_message)\n\n    return json.dumps(transcript, indent=2, ensure_ascii=False)\n\n\ndef _format_text_content(item: dict, include_images: bool = False) -> dict:\n    \"\"\"Format a text content block.\"\"\"\n    return {\n        \"type\": \"text\",\n        \"text\": _clean_text_extraction_markers(item.get(\"text\", \"\"))\n    }\n\n\ndef _format_tool_use_content(item: dict, include_images: bool = False) -> dict:\n    \"\"\"Format a tool use content block.\"\"\"\n    return {\n        \"type\": \"tool_use\",\n        \"name\": item.get(\"name\", \"\"),\n        \"input\": item.get(\"input\", {})\n    }\n\n\ndef _format_tool_result_content(item: dict, include_images: bool = False) -> dict:\n    \"\"\"Format a tool result content block.\"\"\"\n    tool_content = []\n    for content_item in item.get(\"content\", []):\n        if isinstance(content_item, dict):\n            content_type = content_item.get(\"type\")\n            if content_type == \"text\":\n                text = _clean_text_extraction_markers(content_item.get(\"text\", \"\"))\n                tool_content.append({\"type\": \"text\", \"text\": text})\n            elif content_type == \"image\":\n                if include_images:\n                    source = content_item.get(\"source\", {})\n                    if source.get(\"type\") == \"base64\":\n                        tool_content.append({\n                            \"type\": \"image\",\n                            \"media_type\": source.get(\"media_type\", \"image/png\"),\n                            \"base64_data\": source.get(\"data\", \"\")\n                        })\n                else:\n                    tool_content.append({\"type\": \"image\", \"note\": \"Screenshot taken\"})\n\n    return {\n        \"type\": \"tool_result\",\n        \"tool_use_id\": item.get(\"tool_use_id\", \"\"),\n        \"content\": tool_content\n    }\n\n\ndef _format_image_content(item: dict, include_images: bool = False) -> dict:\n    \"\"\"Format an image content block.\"\"\"\n    if include_images:\n        source = item.get(\"source\", {})\n        if source.get(\"type\") == \"base64\":\n            return {\n                \"type\": \"image\",\n                \"media_type\": source.get(\"media_type\", \"image/png\"),\n                \"base64_data\": source.get(\"data\", \"\")\n            }\n    return {\"type\": \"image\", \"note\": \"Image/Screenshot included\"}\n\n\ndef _format_default_content(item: dict, include_images: bool = False) -> dict:\n    \"\"\"Format unknown content types - fallback handler.\"\"\"\n    return item\n\n\n# Strategy pattern: Map content types to their formatting functions\nCONTENT_FORMATTERS = {\n    \"text\": _format_text_content,\n    \"tool_use\": _format_tool_use_content,\n    \"tool_result\": _format_tool_result_content,\n    \"image\": _format_image_content,\n}\n\n\ndef _format_content_item(item, include_images: bool = False):\n    \"\"\"Format a single content item using the appropriate formatter.\n\n    Uses the Strategy pattern to dispatch to the correct formatter based on content type.\n    \"\"\"\n    if not isinstance(item, dict):\n        return str(item)\n\n    content_type = item.get(\"type\")\n    formatter = CONTENT_FORMATTERS.get(content_type, _format_default_content)\n    return formatter(item, include_images)\n\n\ndef _format_message_content(content, include_images: bool = False):\n    \"\"\"Format message content based on its type.\n\n    This is the main entry point that handles different content structures.\n    \"\"\"\n    if isinstance(content, str):\n        return content\n    elif isinstance(content, list):\n        return [_format_content_item(item, include_images) for item in content]\n    else:\n        return str(content)\n\n\ndef authenticate():\n    \"\"\"Handle API key authentication.\"\"\"\n    if st.session_state.provider == APIProvider.ANTHROPIC:\n        if not st.session_state.api_key:\n            st.error(\"Please provide your Anthropic API key in the sidebar\")\n            st.stop()\n    return True\n\n\ndef get_or_create_event_loop():\n    \"\"\"Get existing event loop or create a new one if needed.\n\n    This function ensures we have a valid event loop for async operations,\n    reusing existing loops when possible to avoid Playwright issues with asyncio.run().\n\n    Returns:\n        The active asyncio event loop.\n    \"\"\"\n    if st.session_state.event_loop is None or st.session_state.event_loop.is_closed():\n        st.session_state.event_loop = asyncio.new_event_loop()\n\n    asyncio.set_event_loop(st.session_state.event_loop)\n    return st.session_state.event_loop\n\n\nasync def run_agent(user_input: str):\n    \"\"\"Run the browser automation agent with user input.\"\"\"\n    try:\n        # Ensure chat is disabled while processing\n        st.session_state.chat_disabled = True\n\n        # Create message renderer\n        renderer = MessageRenderer(st.session_state)\n\n        # Add user message to history\n        st.session_state.messages.append({\"role\": \"user\", \"content\": user_input})\n\n        # Display user message in active container\n        with st.session_state.active_response_container:\n            renderer.render(Sender.USER, user_input)\n\n        # Clear active messages for new interaction\n        st.session_state.active_messages = []\n\n        # Prepare messages for API - preserve full conversation history\n        api_messages = list(st.session_state.messages)\n\n        # Setup callbacks for streaming responses\n        def output_callback(content_block: BetaContentBlockParam):\n            \"\"\"Handle agent output - both text and tool use.\"\"\"\n            # Stream to active container in real-time\n            with st.session_state.active_response_container:\n                renderer.render(Sender.BOT, content_block)\n            # Store for later persistence\n            st.session_state.active_messages.append((\"assistant\", content_block))\n\n        def tool_output_callback(result: ToolResult, tool_id: str):\n            \"\"\"Handle tool execution results.\"\"\"\n            st.session_state.tools[tool_id] = result\n            # Stream to active container in real-time\n            with st.session_state.active_response_container:\n                renderer.render(Sender.TOOL, result)\n            # Store for later persistence\n            st.session_state.active_messages.append((\"tool\", result, tool_id))\n\n        def api_response_callback(request, response, error):\n            \"\"\"Handle API responses.\"\"\"\n            if error:\n                with st.session_state.active_response_container:\n                    st.error(f\"API Error: {error}\")\n\n        # Run the agent with persistent browser tool\n        updated_messages = await sampling_loop(\n            model=st.session_state.model,\n            provider=st.session_state.provider,\n            system_prompt_suffix=st.session_state.system_prompt,\n            messages=api_messages,\n            output_callback=output_callback,\n            tool_output_callback=tool_output_callback,\n            api_response_callback=api_response_callback,\n            api_key=st.session_state.api_key,\n            max_tokens=st.session_state.max_tokens,\n            browser_tool=st.session_state.browser_tool,  # Pass persistent browser instance\n            only_n_most_recent_images=3,  # Keep only 3 most recent screenshots for context\n        )\n\n        # Update session state with the complete message history\n        if updated_messages:\n            st.session_state.messages = updated_messages\n\n        # Re-enable chat input\n        st.session_state.chat_disabled = False\n\n        # Trigger a rerun to update the history display\n        st.rerun()\n\n    except RateLimitError:\n        error_msg = \"Rate limit exceeded. Please wait before sending another message.\"\n        st.session_state.last_error = {\"message\": error_msg, \"traceback\": None}\n        with st.session_state.active_response_container:\n            st.error(error_msg)\n        st.session_state.chat_disabled = False\n        st.rerun()\n    except Exception as e:\n        error_msg = f\"Error: {str(e)}\"\n        error_traceback = traceback.format_exc()\n        st.session_state.last_error = {\"message\": error_msg, \"traceback\": error_traceback}\n        with st.session_state.active_response_container:\n            st.error(error_msg)\n            st.code(error_traceback)\n        st.session_state.chat_disabled = False\n        st.rerun()\n\n\ndef main():\n    \"\"\"Main application entry point.\"\"\"\n    st.set_page_config(\n        page_title=\"Claude Browser Use Demo\",\n        page_icon=\"🌐\",\n        layout=\"wide\"\n    )\n\n    st.markdown(STREAMLIT_STYLE, unsafe_allow_html=True)\n\n    setup_state()\n\n\n    # Sidebar configuration\n    with st.sidebar:\n        st.header(\"⚙️ Configuration\")\n\n        # API Provider (fixed to Anthropic for browser use)\n        st.selectbox(\n            \"API Provider\",\n            options=[APIProvider.ANTHROPIC],\n            index=0,\n            key=\"provider\",\n            disabled=True,\n            help=\"Browser Use requires Anthropic API\",\n        )\n\n        # Model selection (only browser-compatible models)\n        st.selectbox(\"Model\", options=BROWSER_COMPATIBLE_MODELS, index=0, key=\"model\")\n\n        # API Key\n        st.text_input(\n            \"Anthropic API Key\",\n            type=\"password\",\n            value=st.session_state.api_key,\n            key=\"api_key\",\n            help=\"Get your API key from https://console.anthropic.com\",\n        )\n\n        # Max tokens\n        st.number_input(\n            \"Max Output Tokens\",\n            min_value=1024,\n            max_value=32768,\n            value=st.session_state.max_tokens,\n            step=1024,\n            key=\"max_tokens\",\n        )\n\n        # System prompt\n        st.text_area(\n            \"Additional System Prompt\",\n            value=st.session_state.system_prompt,\n            key=\"system_prompt\",\n            help=\"Add custom instructions for the browser agent\",\n        )\n\n        # Hide screenshots\n        st.checkbox(\n            \"Hide Screenshots\",\n            value=st.session_state.hide_screenshots,\n            key=\"hide_screenshots\",\n            help=\"Hide screenshot outputs in the chat\",\n        )\n\n        # Conversation Management Section\n        st.divider()\n        st.subheader(\"💬 Conversation\")\n\n        # Download transcript options and button\n        if st.session_state.messages:\n            # Checkbox to include images\n            include_images = st.checkbox(\n                \"Include images in transcript\",\n                value=False,\n                help=\"Include screenshots as separate PNG files in a ZIP archive\"\n            )\n\n            timestamp = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n\n            if include_images:\n                # Generate ZIP with images\n                zip_data = create_transcript_zip(\n                    st.session_state.messages,\n                    include_images=True\n                )\n\n                # Show file size\n                file_size_kb = len(zip_data) / 1024\n                if file_size_kb > 1024:\n                    size_str = f\"{file_size_kb / 1024:.1f} MB\"\n                else:\n                    size_str = f\"{file_size_kb:.1f} KB\"\n\n                st.download_button(\n                    label=f\"📦 Download Transcript ZIP ({size_str})\",\n                    data=zip_data,\n                    file_name=f\"browser_demo_transcript_{timestamp}.zip\",\n                    mime=\"application/zip\",\n                    help=f\"Download conversation with images as ZIP archive ({size_str})\",\n                    type=\"primary\",\n                    use_container_width=True,\n                )\n            else:\n                # Generate JSON only\n                transcript_json = format_transcript_for_download(\n                    st.session_state.messages,\n                    include_images=False\n                )\n\n                # Show file size\n                file_size_kb = len(transcript_json.encode('utf-8')) / 1024\n                if file_size_kb > 1024:\n                    size_str = f\"{file_size_kb / 1024:.1f} MB\"\n                else:\n                    size_str = f\"{file_size_kb:.1f} KB\"\n\n                st.download_button(\n                    label=f\"📄 Download Transcript JSON ({size_str})\",\n                    data=transcript_json,\n                    file_name=f\"browser_demo_transcript_{timestamp}.json\",\n                    mime=\"application/json\",\n                    help=f\"Download conversation transcript as JSON ({size_str})\",\n                    type=\"primary\",\n                    use_container_width=True,\n                )\n        else:\n            st.info(\"No messages to download yet\", icon=\"💬\")\n\n        # Clear conversation\n        if st.button(\"🗑️ Clear Conversation\", type=\"secondary\", use_container_width=True):\n            st.session_state.messages = []\n            st.session_state.tools = {}\n            st.session_state.rendered_message_count = 0\n            st.session_state.active_messages = []\n            st.session_state.chat_disabled = False\n            st.rerun()\n\n        # Reset browser to blank page\n        if st.button(\"Reset Browser\", type=\"secondary\"):\n            async def reset_browser():\n                if st.session_state.browser_tool._page:\n                    await st.session_state.browser_tool._page.goto(\"about:blank\")\n\n            if st.session_state.event_loop is None or st.session_state.event_loop.is_closed():\n                st.session_state.event_loop = asyncio.new_event_loop()\n            asyncio.set_event_loop(st.session_state.event_loop)\n            st.session_state.event_loop.run_until_complete(reset_browser())\n            st.rerun()\n\n    # Main chat interface\n    st.title(\"🌐 Claude Browser Use Demo\")\n    st.markdown(\n        \"This demo showcases Claude's ability to interact with web browsers using \"\n        \"Playwright automation. Ask Claude to navigate websites, fill forms, \"\n        \"extract information, and more!\"\n    )\n\n    # Authenticate\n    if not authenticate():\n        return\n\n\n    # Create container for conversation history\n    history_container = st.container()\n\n    # Display conversation history in the history container\n    renderer = MessageRenderer(st.session_state)\n    with history_container:\n        renderer.render_conversation_history(st.session_state.messages)\n\n    # Create container for active/streaming responses\n    active_container = st.container()\n    st.session_state.active_response_container = active_container\n\n    # Simple callback to disable chat input on submit\n    def disable_chat_callback():\n        st.session_state.chat_disabled = True\n\n    # Show persistent error message if there is one\n    if st.session_state.last_error:\n        st.error(st.session_state.last_error[\"message\"])\n        if st.session_state.last_error[\"traceback\"]:\n            with st.expander(\"Show full traceback\"):\n                st.code(st.session_state.last_error[\"traceback\"])\n        if st.button(\"Clear Error\"):\n            st.session_state.last_error = None\n            st.rerun()\n\n    # Show status when chat is disabled\n    if st.session_state.chat_disabled:\n        st.info(\"🤖 Claude is currently processing your request. Please wait...\")\n\n    # Simple chat input with disabled state\n    prompt = st.chat_input(\n        \"Ask Claude to browse the web...\",\n        disabled=st.session_state.chat_disabled,\n        on_submit=disable_chat_callback\n    )\n\n    if prompt:\n        # Clear any previous error when starting a new request\n        st.session_state.last_error = None\n        # Process the prompt\n        loop = get_or_create_event_loop()\n        loop.run_until_complete(run_agent(prompt))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "browser-use-demo/browser_use_demo/tools/__init__.py",
    "content": "from .base import BaseAnthropicTool, ToolError, ToolResult\nfrom .browser import BrowserTool\nfrom .collection import ToolCollection\n\n__all__ = [\n    \"BaseAnthropicTool\",\n    \"ToolError\",\n    \"ToolResult\",\n    \"BrowserTool\",\n    \"ToolCollection\",\n]\n"
  },
  {
    "path": "browser-use-demo/browser_use_demo/tools/base.py",
    "content": "from abc import ABCMeta, abstractmethod\nfrom dataclasses import dataclass, fields, replace\nfrom typing import Any\n\nfrom anthropic.types.beta import BetaToolUnionParam\n\n\nclass BaseAnthropicTool(metaclass=ABCMeta):\n    \"\"\"Abstract base class for Anthropic-defined tools.\"\"\"\n\n    @abstractmethod\n    def __call__(self, **kwargs) -> Any:\n        \"\"\"Executes the tool with the given arguments.\"\"\"\n        ...\n\n    @abstractmethod\n    def to_params(\n        self,\n    ) -> BetaToolUnionParam:\n        raise NotImplementedError\n\n\n@dataclass(kw_only=True, frozen=True)\nclass ToolResult:\n    \"\"\"Represents the result of a tool execution.\"\"\"\n\n    output: str | None = None\n    error: str | None = None\n    base64_image: str | None = None\n    system: str | None = None\n\n    def __bool__(self):\n        return any(getattr(self, field.name) for field in fields(self))\n\n    def __add__(self, other: \"ToolResult\"):\n        def combine_fields(\n            field: str | None, other_field: str | None, concatenate: bool = True\n        ):\n            if field and other_field:\n                if concatenate:\n                    return field + other_field\n                raise ValueError(\"Cannot combine tool results\")\n            return field or other_field\n\n        return ToolResult(\n            output=combine_fields(self.output, other.output),\n            error=combine_fields(self.error, other.error),\n            base64_image=combine_fields(self.base64_image, other.base64_image, False),\n            system=combine_fields(self.system, other.system),\n        )\n\n    def replace(self, **kwargs):\n        \"\"\"Returns a new ToolResult with the given fields replaced.\"\"\"\n        return replace(self, **kwargs)\n\n\nclass CLIResult(ToolResult):\n    \"\"\"A ToolResult that can be rendered as a CLI output.\"\"\"\n\n\nclass ToolFailure(ToolResult):\n    \"\"\"A ToolResult that represents a failure.\"\"\"\n\n\nclass ToolError(Exception):\n    \"\"\"Raised when a tool encounters an error.\"\"\"\n\n    def __init__(self, message):\n        self.message = message\n"
  },
  {
    "path": "browser-use-demo/browser_use_demo/tools/browser.py",
    "content": "# Modifications Copyright (c) 2025 Anthropic, PBC\n# Modified from original Microsoft Playwright source\n# Original Microsoft Playwright source licensed under Apache License 2.0\n# See CHANGELOG.md for details\n\n\"\"\"Browser automation tool using Playwright for web interaction.\"\"\"\n\nimport asyncio\nimport base64\nimport json\nimport os\nimport sys\nfrom pathlib import Path\nfrom typing import Any, Literal, Optional, TypedDict, cast\nfrom uuid import uuid4\n\nfrom anthropic.types.beta import BetaToolUnionParam\nfrom playwright.async_api import Browser, BrowserContext, Page\n\nfrom ..display_constants import BROWSER_HEIGHT, BROWSER_WIDTH, DISPLAY_NUM\nfrom .base import BaseAnthropicTool, ToolError, ToolResult\nfrom .coordinate_scaling import CoordinateScaler\n\n# Simple logging for debugging - removed, using print directly\n\n\n# Custom browser tool input schema\nBROWSER_TOOL_INPUT_SCHEMA: dict[str, Any] = {\n    \"properties\": {\n        \"action\": {\n            \"description\": 'The action to perform. The available actions are:\\n* `navigate`: Navigate to a URL or use \"back\"/\"forward\" for browser history navigation. Automatically includes a screenshot of the loaded page.\\n* `screenshot`: Take a screenshot of the current browser viewport.\\n* `left_click`: Click the left mouse button at the specified coordinate or element reference.\\n* `right_click`: Click the right mouse button at the specified coordinate or element reference.\\n* `middle_click`: Click the middle mouse button at the specified coordinate or element reference.\\n* `double_click`: Double-click the left mouse button at the specified coordinate or element reference.\\n* `triple_click`: Triple-click the left mouse button at the specified coordinate or element reference.\\n* `hover`: Move the mouse cursor to the specified coordinate or element reference without clicking. Useful for revealing tooltips, dropdown menus, or triggering hover states.\\n* `left_click_drag`: Click and drag from start_coordinate to coordinate.\\n* `left_mouse_down`: Press and hold the left mouse button at the specified coordinate.\\n* `left_mouse_up`: Release the left mouse button at the specified coordinate.\\n* `scroll`: Scroll the page in a specified direction.\\n* `scroll_to`: Scroll to bring an element into view.\\n* `type`: Type text at the current cursor position.\\n* `key`: Press a key or key combination (supports standard keys and modifiers).\\n* `hold_key`: Hold down a key or key combination for a specified duration.\\n* `read_page`: Get the DOM tree structure, optionally filtered for interactive elements.\\n* `find`: Find text on the page and highlight matches.\\n* `get_page_text`: Get all text content from the page.\\n* `wait`: Wait for a specified duration in seconds.\\n* `form_input`: Set the value of a form input element.\\n* `zoom`: Take a zoomed screenshot of a specific region.\\n* `execute_js`: Execute JavaScript code in the page context. Returns the result of the last expression.',\n            \"enum\": [\n                \"navigate\",\n                \"screenshot\",\n                \"left_click\",\n                \"right_click\",\n                \"middle_click\",\n                \"double_click\",\n                \"triple_click\",\n                \"hover\",\n                \"left_click_drag\",\n                \"left_mouse_down\",\n                \"left_mouse_up\",\n                \"scroll\",\n                \"scroll_to\",\n                \"type\",\n                \"key\",\n                \"hold_key\",\n                \"read_page\",\n                \"find\",\n                \"get_page_text\",\n                \"wait\",\n                \"form_input\",\n                \"zoom\",\n                \"execute_js\",\n            ],\n            \"type\": \"string\",\n        },\n        \"text\": {\n            \"description\": 'Required for: `navigate` (URL or \"back\"/\"forward\"), `type` (text to type), `key` (key combination), `hold_key` (key to hold), `find` (text to search), `execute_js` (valid JavaScript code ONLY - no explanatory text, just the code). Optional for `read_page` (filter type: \"interactive\"), click actions (modifier keys to hold during click).',\n            \"type\": \"string\",\n        },\n        \"ref\": {\n            \"description\": \"Element reference string for targeting specific DOM elements. Required for `scroll_to` and `form_input`. Optional for click actions and `hover` as an alternative to coordinates.\",\n            \"type\": \"string\",\n        },\n        \"coordinate\": {\n            \"description\": \"(x, y): The x (pixels from the left edge) and y (pixels from the top edge) coordinates. Required for mouse actions when `ref` is not provided: `left_click`, `right_click`, `middle_click`, `double_click`, `triple_click`, `hover`, `left_mouse_down`, `left_mouse_up`, `scroll`. Also serves as the end coordinate for `left_click_drag`.\",\n            \"type\": \"array\",\n            \"items\": {\"type\": \"integer\"},\n        },\n        \"start_coordinate\": {\n            \"description\": \"(x, y): The starting x and y coordinates for drag operations. Required only for `left_click_drag`.\",\n            \"type\": \"array\",\n            \"items\": {\"type\": \"integer\"},\n        },\n        \"scroll_direction\": {\n            \"description\": \"The direction to scroll. Required for `scroll` action.\",\n            \"enum\": [\"up\", \"down\", \"left\", \"right\"],\n            \"type\": \"string\",\n        },\n        \"scroll_amount\": {\n            \"description\": \"The number of scroll units (similar to mouse wheel clicks). Required for `scroll` action.\",\n            \"type\": \"integer\",\n        },\n        \"duration\": {\n            \"description\": \"Duration in seconds. Required for `hold_key` and `wait` actions. For `wait`, must be between 0 and 100 seconds.\",\n            \"type\": \"number\",\n        },\n        \"value\": {\n            \"description\": \"The value to set for a form input element. Required for `form_input` action. Can be string, number, or boolean depending on the input type.\",\n            \"type\": [\"string\", \"number\", \"boolean\"],\n        },\n        \"region\": {\n            \"description\": \"(x1, y1, x2, y2): Defines a rectangular region for the `zoom` action. Coordinates specify top-left (x1, y1) and bottom-right (x2, y2) corners.\",\n            \"type\": \"array\",\n            \"items\": {\"type\": \"integer\"},\n        },\n    },\n    \"required\": [\"action\"],\n    \"type\": \"object\",\n}\n\nBROWSER_TOOL_DESCRIPTION = \"\"\"A browser automation tool for web interaction. Use this tool to navigate websites, interact with elements, and extract content.\n\nKey actions:\n- navigate: Go to a URL (automatically includes a screenshot)\n- screenshot: Take a visual screenshot\n- read_page: Get DOM structure with element references\n- get_page_text: Extract all text content\n- left_click, right_click, double_click: Click elements\n- hover: Move cursor without clicking (for tooltips, dropdowns)\n- type: Enter text at cursor\n- scroll: Scroll the page\n- form_input: Fill form fields\n- execute_js: Run JavaScript in page context\"\"\"\n\n\nOUTPUT_DIR = Path(\"/tmp/outputs\")\nOUTPUT_DIR.mkdir(parents=True, exist_ok=True)\n\n# Directory containing browser tool utility files (JS scripts)\nBROWSER_TOOL_UTILS_DIR = Path(__file__).parent.parent / \"browser_tool_utils\"\n\n\nclass BrowserOptions(TypedDict):\n    display_width_px: int\n    display_height_px: int\n\n\nActions = Literal[\n    \"navigate\",\n    \"screenshot\",\n    \"left_click\",\n    \"right_click\",\n    \"middle_click\",\n    \"double_click\",\n    \"triple_click\",\n    \"hover\",\n    \"left_click_drag\",\n    \"left_mouse_down\",\n    \"left_mouse_up\",\n    \"scroll\",\n    \"scroll_to\",\n    \"type\",\n    \"key\",\n    \"hold_key\",\n    \"read_page\",\n    \"find\",\n    \"get_page_text\",\n    \"wait\",\n    \"form_input\",\n    \"zoom\",\n    \"execute_js\",\n]\n\n\nclass BrowserTool(BaseAnthropicTool):\n    \"\"\"\n    A browser automation tool using Playwright for web interaction.\n\n    Key actions for extracting content:\n    - read_page: Extract structured DOM tree with element references (USE THIS for analyzing page structure)\n    - get_page_text: Extract all text content from the page (USE THIS for reading articles/posts)\n    - screenshot: Take a visual screenshot (only for visual confirmation, not for reading content)\n\n    Navigation actions:\n    - navigate: Go to a URL\n    - find: Search for elements on the page\n\n    Interaction actions:\n    - left_click, right_click, double_click: Click elements\n    - type: Enter text\n    - scroll: Scroll the page\n    \"\"\"\n\n    name: Literal[\"browser\"] = \"browser\"\n\n    # Instance-level browser connection (recreated per request)\n    _browser: Optional[Browser] = None\n    _context: Optional[BrowserContext] = None\n    _page: Optional[Page] = None\n    _playwright = None\n\n    def __init__(self):\n        \"\"\"Initialize the browser tool with standard viewport dimensions.\"\"\"\n        super().__init__()\n        # Use constants for display configuration\n        self.width = BROWSER_WIDTH\n        self.height = BROWSER_HEIGHT\n        self._initialized = False\n        self._event_loop = None  # Track which event loop we're initialized in\n        self.cdp_url = None  # Initialize CDP URL attribute for cleanup method\n\n    @property\n    def options(self) -> BrowserOptions:\n        \"\"\"Return browser display options.\"\"\"\n        # Note: This implementation uses fixed 1920x1080 dimensions with empirical\n        # coordinate correction. For the recommended approach using client-side\n        # downscaling, see the \"Handle coordinate scaling\" section in the computer\n        # use documentation.\n        return {\n            \"display_width_px\": self.width,\n            \"display_height_px\": self.height,\n        }\n\n    def to_params(self) -> BetaToolUnionParam:\n        \"\"\"Convert tool to API parameters using custom tool definition.\"\"\"\n        return cast(\n            BetaToolUnionParam,\n            {\n                \"name\": self.name,\n                \"description\": BROWSER_TOOL_DESCRIPTION,\n                \"input_schema\": BROWSER_TOOL_INPUT_SCHEMA,\n            },\n        )\n\n    async def _ensure_browser(self) -> None:\n        \"\"\"Launch browser and ensure page is ready.\"\"\"\n        # NOTE: We intentionally DON'T reset the browser if the event loop changes\n        # The browser should persist across conversation turns\n        # Commenting out event loop check that was causing browser resets:\n        # try:\n        #     current_loop = asyncio.get_running_loop()\n        #     if self._initialized and hasattr(self, \"_event_loop\"):\n        #         if self._event_loop != current_loop:\n        #             self._initialized = False\n        #             self._browser = None\n        #             self._context = None\n        #             self._page = None\n        #             self._playwright = None\n        # except RuntimeError:\n        #     pass\n\n        if self._initialized:\n            print(\n                f\"[Browser] Reusing existing browser instance\",\n                file=sys.stderr,\n                flush=True,\n            )\n            if self._page:\n                current_url = self._page.url\n                print(\n                    f\"[Browser] Current page URL: {current_url}\",\n                    file=sys.stderr,\n                    flush=True,\n                )\n\n        if not self._initialized:\n            print(\n                f\"[Browser] Initializing browser for first time\",\n                file=sys.stderr,\n                flush=True,\n            )\n            if self._playwright is None:\n                from playwright.async_api import async_playwright\n\n                self._playwright = await async_playwright().start()\n\n            if self._browser is None:\n                viewport_width = self.width\n                viewport_height = self.height\n\n                is_docker = os.path.exists(\"/.dockerenv\")\n\n                launch_args = [\n                    \"--start-maximized\",\n                    f\"--window-size={viewport_width},{viewport_height}\",\n                    \"--window-position=0,0\",\n                    \"--disable-blink-features=AutomationControlled\",\n                    \"--disable-dev-shm-usage\",\n                    \"--no-sandbox\",\n                    \"--disable-setuid-sandbox\",\n                    \"--disable-gpu-sandbox\",\n                    \"--disable-software-rasterizer\",\n                ]\n\n                if is_docker:\n                    launch_args.extend([\n                        f\"--display=:{DISPLAY_NUM}\",\n                        \"--disable-infobars\",\n                        \"--disable-session-crashed-bubble\",\n                        \"--no-first-run\",\n                        \"--disable-features=TranslateUI\",\n                        \"--disable-component-extensions-with-background-pages\",\n                    ])\n\n                print(\n                    f\"[Browser] Launching browser with viewport {viewport_width}x{viewport_height}\",\n                    file=sys.stderr,\n                    flush=True,\n                )\n\n                self._browser = await self._playwright.chromium.launch(\n                    headless=False,\n                    args=launch_args,\n                )\n\n                self._context = await self._browser.new_context(\n                    viewport={\"width\": viewport_width, \"height\": viewport_height},\n                    user_agent=\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\",\n                )\n                self._page = await self._context.new_page()\n                self._page.set_default_timeout(30000)\n\n                print(\n                    f\"[Browser] Browser initialized with viewport: {viewport_width}x{viewport_height}\",\n                    file=sys.stderr,\n                    flush=True,\n                )\n                print(\n                    f\"[Browser] New browser instance created\",\n                    file=sys.stderr,\n                    flush=True,\n                )\n\n            self._initialized = True\n            try:\n                self._event_loop = asyncio.get_running_loop()\n            except RuntimeError:\n                self._event_loop = None\n\n    async def _execute_js_from_file(self, filename: str, *args) -> Any:\n        \"\"\"Load and execute JavaScript from a file.\"\"\"\n        if self._page is None:\n            raise ToolError(\"Browser not initialized\")\n\n        script_path = BROWSER_TOOL_UTILS_DIR / filename\n        if not script_path.exists():\n            raise ToolError(f\"Script file not found: {filename}\")\n\n        script = script_path.read_text()\n\n        # Special handling for browser_dom_script.js\n        if filename == \"browser_dom_script.js\":\n            # The DOM script defines window.__generateAccessibilityTree function\n            # We need to inject it and then call it\n            filter_type = args[0] if args else \"\"\n            combined_expression = f\"\"\"\n                (function() {{\n                    {script}\n                    return window.__generateAccessibilityTree('{filter_type}');\n                }})()\n            \"\"\"\n            return await self._page.evaluate(combined_expression)\n        else:\n            # For other scripts, wrap as a function and call with arguments\n            escaped_args = \", \".join(json.dumps(arg) for arg in args)\n            js_expression = f\"({script})({escaped_args})\"\n            return await self._page.evaluate(js_expression)\n\n    async def _take_screenshot(self) -> ToolResult:\n        \"\"\"\n        Take a visual screenshot of the current page.\n        NOTE: This only returns an image, not text content.\n        Use read_page or get_page_text to extract actual content.\n        \"\"\"\n        if self._page is None:\n            raise ToolError(\"Browser not initialized\")\n\n        try:\n            # Save screenshot directly to file (like browser.py does with scrot)\n            screenshot_path = OUTPUT_DIR / f\"screenshot_{uuid4().hex}.png\"\n            await self._page.screenshot(path=str(screenshot_path), full_page=False)\n\n            # Read the file and encode to base64\n            screenshot_bytes = screenshot_path.read_bytes()\n            image_base64 = base64.b64encode(screenshot_bytes).decode()\n\n            return ToolResult(output=\"\", error=None, base64_image=image_base64)\n        except Exception as e:\n            raise ToolError(f\"Failed to take screenshot: {str(e)}\") from e\n\n    async def _zoom_screenshot(\n        self, x: int, y: int, width: int, height: int\n    ) -> ToolResult:\n        \"\"\"Take a screenshot of a specific region.\"\"\"\n        if self._page is None:\n            raise ToolError(\"Browser not initialized\")\n\n        try:\n            # Take screenshot with clipping\n            screenshot_path = OUTPUT_DIR / f\"zoom_screenshot_{uuid4().hex}.png\"\n            await self._page.screenshot(\n                path=str(screenshot_path),\n                clip={\"x\": x, \"y\": y, \"width\": width, \"height\": height},\n            )\n\n            # Read the file and encode to base64\n            screenshot_bytes = screenshot_path.read_bytes()\n            image_base64 = base64.b64encode(screenshot_bytes).decode()\n\n            return ToolResult(output=\"\", error=None, base64_image=image_base64)\n        except Exception as e:\n            raise ToolError(f\"Failed to take zoom screenshot: {str(e)}\") from e\n\n    async def _navigate(self, url: str) -> ToolResult:\n        \"\"\"Navigate to a URL.\"\"\"\n        if self._page is None:\n            raise ToolError(\"Browser not initialized\")\n\n        try:\n            # Add protocol if missing\n            if not url.startswith((\"http://\", \"https://\", \"file://\", \"about:\")):\n                url = f\"https://{url}\"\n\n            await self._page.goto(url, wait_until=\"domcontentloaded\")\n            await asyncio.sleep(2)  # Wait for page to stabilize\n\n            # Take screenshot after navigation\n            return await self._take_screenshot()\n\n        except Exception as e:\n            raise ToolError(f\"Failed to navigate to {url}: {str(e)}\") from e\n\n    def _scale_coordinates(self, x: int, y: int) -> tuple[int, int]:\n        \"\"\"\n        Apply auto-scaling to coordinates using the CoordinateScaler.\n\n        Claude's vision model interprets images at a different resolution than actual.\n        We use empirically-derived base resolution for accurate coordinate mapping.\n\n        Args:\n            x: Original x coordinate\n            y: Original y coordinate\n\n        Returns:\n            Tuple of (scaled_x, scaled_y)\n        \"\"\"\n        # Get scale factors for this viewport\n        scale_x, scale_y = CoordinateScaler.get_scale_factors(self.width, self.height)\n\n        # Only log scale factors if they're being initialized\n        if not hasattr(self, '_logged_scale_factors'):\n            print(\n                f\"[Auto-Scale] Using scale factors: {scale_x:.3f}x, {scale_y:.3f}y\",\n                file=sys.stderr,\n                flush=True,\n            )\n            self._logged_scale_factors = True\n\n        # Apply scaling using CoordinateScaler\n        scaled_x, scaled_y = CoordinateScaler.scale_coordinates(\n            x, y, self.width, self.height\n        )\n\n        # Log if scaling was actually applied\n        if scaled_x != x or scaled_y != y:\n            print(\n                f\"[Auto-Scale] Scaled ({x}, {y}) -> ({scaled_x}, {scaled_y})\",\n                file=sys.stderr,\n                flush=True,\n            )\n\n        return scaled_x, scaled_y\n\n    async def _click(\n        self,\n        action: str,\n        coordinate: Optional[tuple[int, int]] = None,\n        ref: Optional[str] = None,\n        text: Optional[str] = None,\n    ) -> ToolResult:\n        \"\"\"Handle various click actions.\"\"\"\n        if self._page is None:\n            raise ToolError(\"Browser not initialized\")\n\n        try:\n            button = \"left\"\n            click_count = 1\n\n            if action == \"right_click\":\n                button = \"right\"\n            elif action == \"middle_click\":\n                button = \"middle\"\n            elif action == \"double_click\":\n                click_count = 2\n            elif action == \"triple_click\":\n                click_count = 3\n\n            if coordinate:\n                x, y = coordinate\n\n                # Apply auto-scaling to coordinates\n                x, y = self._scale_coordinates(x, y)\n\n                # Validate coordinates are within viewport bounds\n                viewport = self._page.viewport_size\n                if viewport:\n                    if x < 0 or x > viewport['width'] or y < 0 or y > viewport['height']:\n                        print(\n                            f\"[Click] WARNING: Coordinates ({x}, {y}) are outside viewport \"\n                            f\"({viewport['width']}x{viewport['height']})\",\n                            file=sys.stderr,\n                            flush=True,\n                        )\n                        # Still attempt the click but warn about potential issues\n                        if x > viewport['width']:\n                            print(\n                                f\"[Click] X coordinate {x} exceeds viewport width {viewport['width']}\",\n                                file=sys.stderr,\n                                flush=True,\n                            )\n                        if y > viewport['height']:\n                            print(\n                                f\"[Click] Y coordinate {y} exceeds viewport height {viewport['height']}\",\n                                file=sys.stderr,\n                                flush=True,\n                            )\n\n                # Ensure the page has focus\n                await self._page.bring_to_front()\n\n                # Move mouse to position and click\n                await self._page.mouse.move(x, y)\n                await asyncio.sleep(0.01)  # Small delay to ensure mouse is positioned\n\n                # Perform the click based on type\n                await self._page.mouse.click(\n                    x, y, button=button, click_count=click_count\n                )\n                return ToolResult(output=f\"Clicked at ({x}, {y})\", error=None)\n            elif ref:\n                # Use the browser_element_script.js to find and click element\n                element_info = await self._execute_js_from_file(\n                    \"browser_element_script.js\", ref\n                )\n\n                if not element_info.get(\"success\", False):\n                    raise ToolError(\n                        element_info.get(\"message\", \"Failed to find element\")\n                    )\n\n                # Get the coordinates from element_info\n                click_x, click_y = element_info[\"coordinates\"]\n\n                # Move to element and click\n                await self._page.mouse.move(click_x, click_y)\n                await asyncio.sleep(0.1)\n                await self._page.mouse.click(\n                    click_x, click_y, button=button, click_count=click_count\n                )\n                return ToolResult(output=f\"Clicked element with ref: {ref}\", error=None)\n            elif text:\n                # Click on element containing text\n                await self._page.click(\n                    f\"text={text}\", button=button, click_count=click_count\n                )\n                return ToolResult(output=f\"Clicked on text: {text}\", error=None)\n            else:\n                raise ToolError(\n                    \"Either coordinate, ref, or text is required for click action\"\n                )\n\n        except Exception as e:\n            raise ToolError(f\"Failed to perform {action}: {str(e)}\") from e\n\n    async def _type_text(self, text: str) -> ToolResult:\n        \"\"\"Type text into the focused element.\"\"\"\n        if self._page is None:\n            raise ToolError(\"Browser not initialized\")\n\n        try:\n            await self._page.keyboard.type(text)\n            return ToolResult(output=f\"Typed: {text}\", error=None)\n        except Exception as e:\n            raise ToolError(f\"Failed to type text: {str(e)}\") from e\n\n    async def _press_key(\n        self, key: str, hold: bool = False, duration: float = 0.01\n    ) -> ToolResult:\n        \"\"\"Press a keyboard key or key combination.\"\"\"\n        if self._page is None:\n            raise ToolError(\"Browser not initialized\")\n\n        try:\n            # Load the key map\n            from ..browser_tool_utils.browser_key_map import KEY_MAP\n\n            def map_key(k: str) -> str:\n                \"\"\"Map a key name to Playwright's expected format.\"\"\"\n                key_info = KEY_MAP.get(k.lower())\n                if key_info and \"key\" in key_info:\n                    return key_info[\"key\"]\n                return k\n\n            # Handle key combinations (e.g., \"cmd+a\", \"ctrl+c\")\n            if \"+\" in key:\n                parts = key.split(\"+\")\n                mapped_parts = [map_key(p) for p in parts]\n                mapped_key = \"+\".join(mapped_parts)\n                await self._page.keyboard.press(mapped_key)\n                return ToolResult(output=f\"Pressed key combination: {mapped_key}\", error=None)\n\n            # Map single key if needed\n            key_info = KEY_MAP.get(key.lower())\n            if key_info:\n                key_to_press = key_info[\"code\"] if \"code\" in key_info else key\n            else:\n                key_to_press = key\n\n            if hold:\n                await self._page.keyboard.down(key_to_press)\n                await asyncio.sleep(duration)\n                await self._page.keyboard.up(key_to_press)\n                return ToolResult(\n                    output=f\"Held key '{key}' for {duration} seconds\", error=None\n                )\n            else:\n                await self._page.keyboard.press(key_to_press)\n                return ToolResult(output=f\"Pressed key: {key}\", error=None)\n\n        except Exception as e:\n            raise ToolError(f\"Failed to press key '{key}': {str(e)}\") from e\n\n    async def _scroll(\n        self,\n        coordinate: Optional[tuple[int, int]] = None,\n        direction: Optional[str] = None,\n        amount: Optional[int] = None,\n    ) -> ToolResult:\n        \"\"\"Scroll the page or element.\"\"\"\n        if self._page is None:\n            raise ToolError(\"Browser not initialized\")\n\n        try:\n            if not direction:\n                direction = \"down\"\n            if not amount:\n                amount = 3  # Default scroll amount\n\n            # Calculate scroll delta based on direction\n            delta_x = 0\n            delta_y = 0\n\n            if direction == \"up\":\n                delta_y = -amount * 100\n            elif direction == \"down\":\n                delta_y = amount * 100\n            elif direction == \"left\":\n                delta_x = -amount * 100\n            elif direction == \"right\":\n                delta_x = amount * 100\n\n            if coordinate:\n                x, y = coordinate\n                await self._page.mouse.wheel(delta_x, delta_y)\n            else:\n                # Scroll the main page\n                await self._page.evaluate(f\"window.scrollBy({delta_x}, {delta_y})\")\n\n            # Wait for content to stabilize after scroll\n            await asyncio.sleep(0.5)\n\n            # Take screenshot to show new viewport content\n            screenshot_result = await self._take_screenshot()\n            return ToolResult(\n                output=f\"Scrolled {direction} by {amount} units\",\n                error=None,\n                base64_image=screenshot_result.base64_image\n            )\n\n        except Exception as e:\n            raise ToolError(f\"Failed to scroll: {str(e)}\") from e\n\n    async def _scroll_to(self, ref: str) -> ToolResult:\n        \"\"\"Scroll to a specific element.\"\"\"\n        if self._page is None:\n            raise ToolError(\"Browser not initialized\")\n\n        try:\n            element_info = await self._execute_js_from_file(\n                \"browser_element_script.js\", ref\n            )\n\n            if not element_info[\"success\"]:\n                raise ToolError(element_info.get(\"message\", \"Failed to find element\"))\n\n            # Wait for content to stabilize after scroll\n            await asyncio.sleep(0.5)\n\n            # Take screenshot to show new viewport content\n            screenshot_result = await self._take_screenshot()\n            return ToolResult(\n                output=f\"Scrolled to element with ref: {ref}\",\n                error=None,\n                base64_image=screenshot_result.base64_image\n            )\n\n        except Exception as e:\n            raise ToolError(f\"Failed to scroll to element: {str(e)}\") from e\n\n    async def _drag(\n        self, start_x: int, start_y: int, end_x: int, end_y: int\n    ) -> ToolResult:\n        \"\"\"Perform a drag operation.\"\"\"\n        if self._page is None:\n            raise ToolError(\"Browser not initialized\")\n\n        try:\n            # Apply auto-scaling to both start and end coordinates\n            scaled_start_x, scaled_start_y = self._scale_coordinates(start_x, start_y)\n            scaled_end_x, scaled_end_y = self._scale_coordinates(end_x, end_y)\n\n            await self._page.mouse.move(scaled_start_x, scaled_start_y)\n            await self._page.mouse.down()\n            await self._page.mouse.move(scaled_end_x, scaled_end_y)\n            await self._page.mouse.up()\n\n            return ToolResult(\n                output=f\"Dragged from ({scaled_start_x}, {scaled_start_y}) to ({scaled_end_x}, {scaled_end_y})\",\n                error=None,\n            )\n\n        except Exception as e:\n            raise ToolError(f\"Failed to perform drag: {str(e)}\") from e\n\n    async def _mouse_down(self, x: int, y: int) -> ToolResult:\n        \"\"\"Press mouse button down.\"\"\"\n        if self._page is None:\n            raise ToolError(\"Browser not initialized\")\n\n        try:\n            # Apply auto-scaling to coordinates\n            scaled_x, scaled_y = self._scale_coordinates(x, y)\n\n            await self._page.mouse.move(scaled_x, scaled_y)\n            await self._page.mouse.down()\n            return ToolResult(output=f\"Mouse down at ({scaled_x}, {scaled_y})\", error=None)\n\n        except Exception as e:\n            raise ToolError(f\"Failed to perform mouse down: {str(e)}\") from e\n\n    async def _mouse_up(self, x: int, y: int) -> ToolResult:\n        \"\"\"Release mouse button.\"\"\"\n        if self._page is None:\n            raise ToolError(\"Browser not initialized\")\n\n        try:\n            # Apply auto-scaling to coordinates\n            scaled_x, scaled_y = self._scale_coordinates(x, y)\n\n            await self._page.mouse.move(scaled_x, scaled_y)\n            await self._page.mouse.up()\n            return ToolResult(output=f\"Mouse up at ({scaled_x}, {scaled_y})\", error=None)\n\n        except Exception as e:\n            raise ToolError(f\"Failed to perform mouse up: {str(e)}\") from e\n\n    async def _hover(\n        self,\n        coordinate: Optional[tuple[int, int]] = None,\n        ref: Optional[str] = None,\n    ) -> ToolResult:\n        \"\"\"\n        Move the mouse cursor to a position without clicking.\n        Useful for revealing tooltips, dropdown menus, or triggering hover states.\n        \"\"\"\n        if self._page is None:\n            raise ToolError(\"Browser not initialized\")\n\n        try:\n            # Prefer ref over coordinate (refs are more reliable)\n            if ref:\n                # Use the browser_element_script.js to find element coordinates\n                element_info = await self._execute_js_from_file(\n                    \"browser_element_script.js\", ref\n                )\n\n                if not element_info.get(\"success\", False):\n                    raise ToolError(\n                        element_info.get(\"message\", \"Failed to find element\")\n                    )\n\n                # Get the coordinates from element_info\n                hover_x, hover_y = element_info[\"coordinates\"]\n\n                await self._page.bring_to_front()\n                await self._page.mouse.move(hover_x, hover_y)\n                # Wait for hover effects to render\n                await asyncio.sleep(0.5)\n                # Take screenshot to show hover result\n                screenshot_result = await self._take_screenshot()\n                return ToolResult(\n                    output=f\"Hovered over element with ref: {ref}\",\n                    error=None,\n                    base64_image=screenshot_result.base64_image\n                )\n            elif coordinate:\n                x, y = coordinate\n                # Apply auto-scaling to coordinates\n                scaled_x, scaled_y = self._scale_coordinates(x, y)\n\n                await self._page.bring_to_front()\n                await self._page.mouse.move(scaled_x, scaled_y)\n\n                # Wait for hover effects to render\n                await asyncio.sleep(0.3)\n                # Take screenshot to show hover result\n                screenshot_result = await self._take_screenshot()\n                return ToolResult(\n                    output=f\"Hovered at ({scaled_x}, {scaled_y})\",\n                    error=None,\n                    base64_image=screenshot_result.base64_image\n                )\n            else:\n                raise ToolError(\n                    \"Either coordinate or ref is required for hover action\"\n                )\n\n        except Exception as e:\n            raise ToolError(f\"Failed to perform hover: {str(e)}\") from e\n\n    async def _read_page(self, filter_type: str = \"\") -> ToolResult:\n        \"\"\"\n        Extract the DOM tree with structured content and element references.\n        USE THIS to analyze page structure and find specific elements.\n        Returns a structured tree with text content, not just a screenshot.\n        \"\"\"\n        if self._page is None:\n            raise ToolError(\"Browser not initialized\")\n\n        try:\n            # Use the browser_dom_script.js from reference implementation\n            dom_tree = await self._execute_js_from_file(\n                \"browser_dom_script.js\", filter_type\n            )\n\n            # The script returns {pageContent: string}, extract just the pageContent\n            if isinstance(dom_tree, dict) and \"pageContent\" in dom_tree:\n                full_content = dom_tree[\"pageContent\"]\n            elif isinstance(dom_tree, dict):\n                full_content = json.dumps(dom_tree, indent=2)\n            else:\n                full_content = str(dom_tree)\n\n            # Calculate content size for summary\n            content_length = len(full_content)\n            # Estimate token count\n            # Note: For exact counts, use client.beta.messages.count_tokens API\n            # This estimate uses ~3.5 chars/token which is typical for Claude with English text\n            # Actual ratio varies by content type (code, languages, special characters)\n            estimated_tokens = int(content_length / 3.5)\n\n            # Create a summary for UI display\n            summary = f\"Extracted page DOM tree (~{estimated_tokens:,} tokens, {content_length:,} characters)\"\n\n            # Return the full content for the API but with a marker for the UI\n            return ToolResult(\n                output=f\"__PAGE_EXTRACTED__\\n{summary}\\n__FULL_CONTENT__\\n{full_content}\",\n                error=None\n            )\n\n        except Exception as e:\n            raise ToolError(f\"Failed to read page: {str(e)}\") from e\n\n    async def _get_page_text(self) -> ToolResult:\n        \"\"\"\n        Extract ALL text content from the current page.\n        USE THIS to read articles, posts, or any text content.\n        Returns the actual text, not a screenshot.\n        Perfect for reading Reddit posts, articles, etc.\n        \"\"\"\n        if self._page is None:\n            raise ToolError(\"Browser not initialized\")\n\n        try:\n            # Use the browser_text_script.js from reference implementation\n            result = await self._execute_js_from_file(\"browser_text_script.js\")\n\n            # Format the output like the reference implementation\n            if isinstance(result, dict):\n                full_content = f\"\"\"Title: {result.get(\"title\", \"N/A\")}\nURL: {result.get(\"url\", \"N/A\")}\nSource element: <{result.get(\"source\", \"unknown\")}>\n---\n{result.get(\"text\", \"\")}\"\"\"\n            else:\n                full_content = str(result)\n\n            # Calculate content size for summary\n            content_length = len(full_content)\n            # Estimate token count\n            # Note: For exact counts, use client.beta.messages.count_tokens API\n            # This estimate uses ~3.5 chars/token which is typical for Claude with English text\n            # Actual ratio varies by content type (code, languages, special characters)\n            estimated_tokens = int(content_length / 3.5)\n\n            # Create a summary for UI display\n            title = result.get(\"title\", \"N/A\") if isinstance(result, dict) else \"N/A\"\n            url = result.get(\"url\", \"N/A\") if isinstance(result, dict) else \"N/A\"\n            summary = f\"Extracted page text from: {title}\\nURL: {url}\\n(~{estimated_tokens:,} tokens, {content_length:,} characters)\"\n\n            # Return the full content for the API but with a marker for the UI\n            return ToolResult(\n                output=f\"__TEXT_EXTRACTED__\\n{summary}\\n__FULL_CONTENT__\\n{full_content}\",\n                error=None\n            )\n\n        except Exception as e:\n            raise ToolError(f\"Failed to get page text: {str(e)}\") from e\n\n    async def _find(self, search_query: str) -> ToolResult:\n        \"\"\"Find elements on the page matching the search query using AI.\"\"\"\n        if self._page is None:\n            raise ToolError(\"Browser not initialized\")\n\n        try:\n            # First get the DOM tree for analysis\n            dom_tree = await self._execute_js_from_file(\"browser_dom_script.js\", \"all\")\n\n            if isinstance(dom_tree, dict) and \"pageContent\" in dom_tree:\n                dom_tree_json = dom_tree[\"pageContent\"]\n            else:\n                dom_tree_json = json.dumps(dom_tree, indent=2)\n\n            # Try to use Anthropic API if available\n            api_key = os.environ.get(\"ANTHROPIC_API_KEY\")\n            if api_key:\n                try:\n                    from anthropic import AsyncAnthropic\n\n                    client = AsyncAnthropic(api_key=api_key)\n\n                    prompt = f\"\"\"You are helping find elements on a web page. The user wants to find: \"{search_query}\"\n\nHere is the accessibility tree of the page:\n{dom_tree_json}\n\nFind ALL elements that match the user's query. Return up to 20 most relevant matches, ordered by relevance.\n\nReturn your findings in this exact format (one line per matching element):\n\nFOUND: <total_number_of_matching_elements>\nSHOWING: <number_shown_up_to_20>\n---\nref_X | role | name | type | reason why this matches\nref_Y | role | name | type | reason why this matches\n...\n\nIf there are more than 20 matches, add this line at the end:\nMORE: Use a more specific query to see additional results\n\nIf no matching elements are found, return only:\nFOUND: 0\nERROR: explanation of why no elements were found\"\"\"\n\n                    response = await client.messages.create(\n                        model=\"claude-3-5-sonnet-20241022\",\n                        max_tokens=800,\n                        temperature=1.0,\n                        messages=[{\"role\": \"user\", \"content\": prompt}],\n                    )\n\n                    # Handle the response properly\n                    first_content = response.content[0]\n                    if hasattr(first_content, \"text\"):\n                        response_text = first_content.text.strip()\n                    else:\n                        # Handle other content types if needed\n                        response_text = str(first_content)\n                    lines = [\n                        line.strip()\n                        for line in response_text.split(\"\\n\")\n                        if line.strip()\n                    ]\n\n                    total_found = 0\n                    elements = []\n                    has_more = False\n                    error_message = None\n\n                    for line in lines:\n                        if line.startswith(\"FOUND:\"):\n                            try:\n                                total_found = int(line.split(\":\")[1].strip())\n                            except (ValueError, IndexError):\n                                total_found = 0\n                        elif line.startswith(\"SHOWING:\"):\n                            pass\n                        elif line.startswith(\"ERROR:\"):\n                            error_message = line[6:].strip()\n                        elif line.startswith(\"MORE:\"):\n                            has_more = True\n                        elif line.startswith(\"ref_\") and \"|\" in line:\n                            parts = [p.strip() for p in line.split(\"|\")]\n                            if len(parts) >= 4:\n                                elements.append(\n                                    {\n                                        \"ref\": parts[0],\n                                        \"role\": parts[1],\n                                        \"name\": parts[2] if len(parts) > 2 else \"\",\n                                        \"type\": parts[3] if len(parts) > 3 else \"\",\n                                        \"description\": parts[4]\n                                        if len(parts) > 4\n                                        else \"\",\n                                    }\n                                )\n\n                    if total_found == 0 or len(elements) == 0:\n                        return ToolResult(\n                            output=error_message or \"No matching elements found\",\n                            error=None,\n                        )\n\n                    message = f\"Found {total_found} matching element{'s' if total_found != 1 else ''}\"\n                    if has_more:\n                        message += f\" (showing first {len(elements)}, use a more specific query to narrow results)\"\n\n                    # Format elements for output\n                    elements_output = []\n                    for el in elements:\n                        element_str = f\"- {el['ref']}: {el['role']}\"\n                        if el.get(\"name\"):\n                            element_str += f\" {el['name']}\"\n                        if el.get(\"type\"):\n                            element_str += f\" {el['type']}\"\n                        if el.get(\"description\"):\n                            element_str += f\" - {el['description']}\"\n                        elements_output.append(element_str)\n\n                    elements_str = \"\\n\".join(elements_output)\n                    return ToolResult(output=f\"{message}\\n\\n{elements_str}\", error=None)\n\n                except Exception:\n                    pass  # Failed to use AI for find, falling back to simple search\n\n            # Fallback to simple text search if AI is not available\n            elements = await self._page.query_selector_all(\n                f\"*:has-text('{search_query}')\"\n            )\n\n            if not elements:\n                return ToolResult(\n                    output=f\"No matching elements found for: {search_query}\", error=None\n                )\n\n            # For simple fallback, just report count (no ref_ids without AI analysis)\n            return ToolResult(\n                output=f\"Found {len(elements)} matching element{'s' if len(elements) != 1 else ''} (Note: AI-based search with ref_ids requires ANTHROPIC_API_KEY)\",\n                error=None,\n            )\n\n        except Exception as e:\n            raise ToolError(f\"Failed to find elements: {str(e)}\") from e\n\n    async def _form_input(self, ref: str, value: Any) -> ToolResult:\n        \"\"\"Fill a form field with a value.\"\"\"\n        if self._page is None:\n            raise ToolError(\"Browser not initialized\")\n\n        try:\n            # Use the browser_form_input_script.js from reference implementation\n            result = await self._execute_js_from_file(\n                \"browser_form_input_script.js\", ref, value\n            )\n\n            if isinstance(result, dict) and not result.get(\"success\", False):\n                raise ToolError(result.get(\"message\", \"Failed to fill form field\"))\n\n            return ToolResult(\n                output=f\"Filled form field {ref} with value: {value}\", error=None\n            )\n\n        except Exception as e:\n            raise ToolError(f\"Failed to fill form field: {str(e)}\") from e\n\n    async def _wait(self, duration: float) -> ToolResult:\n        \"\"\"Wait for a specified duration.\"\"\"\n        try:\n            await asyncio.sleep(duration)\n            return ToolResult(\n                output=f\"Waited for {duration} second{'s' if duration != 1 else ''}\",\n                error=None,\n            )\n        except Exception as e:\n            raise ToolError(f\"Failed to wait: {str(e)}\") from e\n\n    async def _execute_js(self, code: str) -> ToolResult:\n        \"\"\"\n        Execute JavaScript code in the page context.\n        Returns the result of the last expression.\n        \"\"\"\n        if self._page is None:\n            raise ToolError(\"Browser not initialized\")\n\n        try:\n            # Execute the code in page context\n            # Playwright's evaluate handles async/await automatically\n            result = await self._page.evaluate(code)\n\n            # Format the result\n            if result is None:\n                result_str = \"undefined\"\n            elif isinstance(result, (dict, list)):\n                result_str = json.dumps(result, indent=2)\n            else:\n                result_str = str(result)\n\n            return ToolResult(output=result_str, error=None)\n\n        except Exception as e:\n            raise ToolError(f\"JavaScript execution error: {str(e)}\") from e\n\n    async def __call__(\n        self,\n        *,\n        action: Actions,\n        text: Optional[str] = None,\n        ref: Optional[str] = None,\n        coordinate: Optional[tuple[int, int]] = None,\n        start_coordinate: Optional[tuple[int, int]] = None,\n        scroll_direction: Optional[Literal[\"up\", \"down\", \"left\", \"right\"]] = None,\n        scroll_amount: Optional[int] = None,\n        duration: Optional[float] = None,\n        value: Optional[Any] = None,\n        region: Optional[tuple[int, int, int, int]] = None,\n        **kwargs,\n    ) -> ToolResult:\n        \"\"\"\n        Execute browser actions.\n\n        Parameters:\n        - action: The action to perform\n        - text: Text input for type, key, navigate, find actions\n        - ref: Element reference for element-based actions\n        - coordinate: (x, y) coordinates for mouse actions\n        - start_coordinate: Starting point for drag actions\n        - scroll_direction: Direction for scroll action\n        - scroll_amount: Amount to scroll\n        - duration: Duration for wait or hold_key actions\n        - value: Value for form_input action\n        - region: (x, y, width, height) for zoom screenshot\n        \"\"\"\n\n        # Ensure browser is running for all actions\n        await self._ensure_browser()\n\n        if action == \"navigate\":\n            if not text:\n                raise ToolError(\"URL is required for navigate action\")\n            return await self._navigate(text)\n\n        elif action == \"screenshot\":\n            return await self._take_screenshot()\n\n        elif action == \"zoom\":\n            if not region:\n                raise ToolError(\n                    \"Region (x1, y1, x2, y2) is required for zoom action\"\n                )\n            x1, y1, x2, y2 = region\n            # Convert corner coordinates to x, y, width, height\n            x = min(x1, x2)\n            y = min(y1, y2)\n            width = abs(x2 - x1)\n            height = abs(y2 - y1)\n            return await self._zoom_screenshot(x, y, width, height)\n\n        elif action in [\n            \"left_click\",\n            \"right_click\",\n            \"middle_click\",\n            \"double_click\",\n            \"triple_click\",\n        ]:\n            return await self._click(action, coordinate, ref, text)\n\n        elif action == \"hover\":\n            return await self._hover(coordinate, ref)\n\n        elif action == \"type\":\n            if not text:\n                raise ToolError(\"Text is required for type action\")\n            return await self._type_text(text)\n\n        elif action == \"key\":\n            if not text:\n                raise ToolError(\"Key is required for key action\")\n            return await self._press_key(text)\n\n        elif action == \"hold_key\":\n            if not text:\n                raise ToolError(\"Key is required for hold_key action\")\n            if not duration:\n                duration = 1.0\n            return await self._press_key(text, hold=True, duration=duration)\n\n        elif action == \"scroll\":\n            return await self._scroll(coordinate, scroll_direction, scroll_amount)\n\n        elif action == \"scroll_to\":\n            if not ref:\n                raise ToolError(\"Element reference is required for scroll_to action\")\n            return await self._scroll_to(ref)\n\n        elif action == \"left_click_drag\":\n            if not start_coordinate or not coordinate:\n                raise ToolError(\n                    \"Both start_coordinate and coordinate are required for drag action\"\n                )\n            start_x, start_y = start_coordinate\n            end_x, end_y = coordinate\n            return await self._drag(start_x, start_y, end_x, end_y)\n\n        elif action == \"left_mouse_down\":\n            if not coordinate:\n                raise ToolError(\"Coordinate is required for mouse_down action\")\n            x, y = coordinate\n            return await self._mouse_down(x, y)\n\n        elif action == \"left_mouse_up\":\n            if not coordinate:\n                raise ToolError(\"Coordinate is required for mouse_up action\")\n            x, y = coordinate\n            return await self._mouse_up(x, y)\n\n        elif action == \"read_page\":\n            filter_type = text if text in [\"interactive\", \"\"] else \"\"\n            return await self._read_page(filter_type)\n\n        elif action == \"get_page_text\":\n            return await self._get_page_text()\n\n        elif action == \"find\":\n            if not text:\n                raise ToolError(\"Text is required for find action\")\n            return await self._find(text)\n\n        elif action == \"form_input\":\n            if not ref:\n                raise ToolError(\"Element reference is required for form_input action\")\n            if value is None:\n                raise ToolError(\"Value is required for form_input action\")\n            return await self._form_input(ref, value)\n\n        elif action == \"wait\":\n            if not duration:\n                duration = 1.0\n            return await self._wait(duration)\n\n        elif action == \"execute_js\":\n            if not text:\n                raise ToolError(\"JavaScript code is required for execute_js action\")\n            return await self._execute_js(text)\n\n        else:\n            raise ToolError(f\"Unknown action: {action}\")\n\n    async def cleanup(self):\n        \"\"\"Cleanup method to ensure browser is closed properly.\"\"\"\n        # Clean up browser resources\n        if self.cdp_url:\n            # When connected to CDP server, just disconnect without closing tabs\n            self._page = None\n            self._context = None\n            self._browser = None\n        else:\n            # For local browser, close everything\n            if self._page:\n                await self._page.close()\n                self._page = None\n\n            if self._context:\n                await self._context.close()\n                self._context = None\n\n            if self._browser:\n                await self._browser.close()\n                self._browser = None\n\n        if self._playwright:\n            await self._playwright.stop()\n            self._playwright = None\n\n        self._initialized = False\n"
  },
  {
    "path": "browser-use-demo/browser_use_demo/tools/collection.py",
    "content": "from typing import Any\n\nfrom anthropic.types.beta import BetaToolUnionParam\n\nfrom .base import BaseAnthropicTool\n\n\nclass ToolCollection:\n    \"\"\"Collection of tools for browser automation.\"\"\"\n\n    def __init__(self, *tools: BaseAnthropicTool):\n        self.tools = tools\n        self.tool_map = {tool.name: tool for tool in tools}\n\n    def to_params(self) -> list[BetaToolUnionParam]:\n        \"\"\"Convert all tools to API parameters.\"\"\"\n        return [tool.to_params() for tool in self.tools]\n"
  },
  {
    "path": "browser-use-demo/browser_use_demo/tools/coordinate_scaling.py",
    "content": "\"\"\"\nCoordinate scaling utilities for browser tool.\n\nThis module handles the scaling of coordinates from Claude's vision model\nresolution to the actual browser viewport resolution.\n\"\"\"\n\n\nclass CoordinateScaler:\n    \"\"\"Handles coordinate scaling between Claude's vision and actual viewport.\"\"\"\n\n    # Claude's image processing resolution for 16:9 aspect ratio\n    # According to the official documentation:\n    # https://docs.claude.com/en/docs/build-with-claude/vision#evaluate-image-size\n    # When images exceed size thresholds, they are resized while preserving aspect ratio.\n    # For 16:9 aspect ratio (like 1920x1080 screenshots), images are resized to these exact dimensions:\n    CLAUDE_ACTUAL_WIDTH = 1456  # Width for 16:9 landscape (from documentation)\n    CLAUDE_ACTUAL_HEIGHT = 819  # Height for 16:9 landscape (from documentation)\n\n    # Documented maximum image sizes for different aspect ratios\n    # Source: https://docs.claude.com/en/docs/build-with-claude/vision#evaluate-image-size\n    DOCUMENTED_SIZES = {\n        # aspect_ratio: (width, height)\n        (1, 1): (1092, 1092),      # Square\n        (3, 4): (951, 1268),       # Portrait\n        (4, 3): (1268, 951),       # Landscape\n        (2, 3): (896, 1344),       # Portrait\n        (3, 2): (1344, 896),       # Landscape\n        (9, 16): (819, 1456),      # Portrait (phone)\n        (16, 9): (1456, 819),      # Landscape (widescreen)\n        (1, 2): (784, 1568),       # Portrait (tall)\n        (2, 1): (1568, 784),       # Landscape (wide)\n    }\n\n    @classmethod\n    def get_documented_size_for_aspect_ratio(cls, viewport_width: int, viewport_height: int) -> tuple[int, int]:\n        \"\"\"\n        Get the documented size for the given viewport's aspect ratio.\n\n        Args:\n            viewport_width: Actual viewport width\n            viewport_height: Actual viewport height\n\n        Returns:\n            Tuple of (width, height) from documented sizes that matches the aspect ratio\n\n        Raises:\n            ValueError: If the aspect ratio doesn't match any documented sizes\n        \"\"\"\n        viewport_ratio = viewport_width / viewport_height\n\n        # Tolerance for aspect ratio matching (to handle minor differences)\n        ASPECT_RATIO_TOLERANCE = 0.02\n\n        # Try to find an exact or very close aspect ratio match\n        for (ratio_w, ratio_h), (doc_width, doc_height) in cls.DOCUMENTED_SIZES.items():\n            doc_ratio = ratio_w / ratio_h\n            if abs(viewport_ratio - doc_ratio) < ASPECT_RATIO_TOLERANCE:\n                return (doc_width, doc_height)\n\n        # If no match found, raise an error with helpful information\n        supported_ratios = [f\"{w}:{h} ({w/h:.3f})\" for (w, h) in cls.DOCUMENTED_SIZES.keys()]\n        raise ValueError(\n            f\"Viewport aspect ratio {viewport_ratio:.3f} ({viewport_width}x{viewport_height}) \"\n            f\"does not match any documented aspect ratios. \"\n            f\"Supported ratios: {', '.join(supported_ratios)}. \"\n            f\"See https://docs.claude.com/en/docs/build-with-claude/vision#evaluate-image-size\"\n        )\n\n    @classmethod\n    def get_scale_factors(\n        cls,\n        viewport_width: int,\n        viewport_height: int,\n        match_aspect_ratio: bool = False\n    ) -> tuple[float, float]:\n        \"\"\"\n        Calculate scale factors for converting Claude coordinates to viewport coordinates.\n\n        Args:\n            viewport_width: Actual browser viewport width\n            viewport_height: Actual browser viewport height\n            match_aspect_ratio: If True, match the viewport's aspect ratio to documented sizes.\n                              If False (default), use 16:9 for browser scenarios.\n\n        Returns:\n            Tuple of (scale_x, scale_y) factors\n\n        Raises:\n            ValueError: If match_aspect_ratio is True and aspect ratio doesn't match documented sizes\n        \"\"\"\n        if match_aspect_ratio:\n            # Match the documented size based on aspect ratio (will raise if no match)\n            base_width, base_height = cls.get_documented_size_for_aspect_ratio(viewport_width, viewport_height)\n        else:\n            # Use default 16:9 dimensions (typical for browser screenshots)\n            base_width = cls.CLAUDE_ACTUAL_WIDTH\n            base_height = cls.CLAUDE_ACTUAL_HEIGHT\n\n        # Calculate scale factors\n        scale_x = viewport_width / base_width\n        scale_y = viewport_height / base_height\n\n        return scale_x, scale_y\n\n    @classmethod\n    def scale_coordinates(\n        cls,\n        x: int,\n        y: int,\n        viewport_width: int,\n        viewport_height: int,\n        apply_threshold: bool = True\n    ) -> tuple[int, int]:\n        \"\"\"\n        Scale coordinates from Claude's vision to actual viewport.\n\n        Args:\n            x: X coordinate from Claude\n            y: Y coordinate from Claude\n            viewport_width: Actual browser viewport width\n            viewport_height: Actual browser viewport height\n            apply_threshold: Whether to check if coordinates need scaling\n\n        Returns:\n            Tuple of (scaled_x, scaled_y)\n        \"\"\"\n        scale_x, scale_y = cls.get_scale_factors(viewport_width, viewport_height)\n\n        # If scaling factors are close to 1.0, no scaling needed\n        if abs(scale_x - 1.0) < 0.05 and abs(scale_y - 1.0) < 0.05:\n            return x, y\n\n        if apply_threshold:\n            # Check if coordinates appear to be in Claude's resolution\n            # (with 20% margin for edge cases)\n            max_expected_x = cls.CLAUDE_ACTUAL_WIDTH * 1.2\n            max_expected_y = cls.CLAUDE_ACTUAL_HEIGHT * 1.2\n\n            # If coordinates are larger than expected Claude resolution,\n            # they might already be in viewport coordinates\n            if x > max_expected_x or y > max_expected_y:\n                return x, y\n\n        # Apply scaling\n        scaled_x = int(x * scale_x)\n        scaled_y = int(y * scale_y)\n\n        # Ensure we don't exceed viewport bounds\n        scaled_x = min(scaled_x, viewport_width - 1)\n        scaled_y = min(scaled_y, viewport_height - 1)\n\n        return scaled_x, scaled_y\n\n    @classmethod\n    def scale_coordinate_list(\n        cls,\n        coords: list | tuple,\n        viewport_width: int,\n        viewport_height: int\n    ) -> list:\n        \"\"\"\n        Scale a coordinate pair list/tuple.\n\n        Args:\n            coords: [x, y] coordinate pair\n            viewport_width: Actual browser viewport width\n            viewport_height: Actual browser viewport height\n\n        Returns:\n            Scaled [x, y] coordinate pair\n        \"\"\"\n        if not isinstance(coords, (list, tuple)) or len(coords) != 2:\n            return list(coords) if isinstance(coords, tuple) else coords\n\n        x, y = coords[0], coords[1]\n        scaled_x, scaled_y = cls.scale_coordinates(x, y, viewport_width, viewport_height)\n        return [scaled_x, scaled_y]"
  },
  {
    "path": "browser-use-demo/build.sh",
    "content": "#!/bin/bash\nset -e\n\necho \"🏗️  Building Browser Use Demo Docker image...\"\ndocker build . -t browser-use-demo:latest\n\necho \"✅ Build complete!\"\necho \"\"\necho \"To run the demo:\"\necho \"  docker run -e ANTHROPIC_API_KEY=\\$ANTHROPIC_API_KEY \\\\\"\necho \"    -v \\$(pwd)/browser_use_demo:/home/browseruse/browser_use_demo/ \\\\\"\necho \"    -p 5900:5900 -p 8501:8501 -p 6080:6080 -p 8080:8080 \\\\\"\necho \"    -it browser-use-demo:latest\"\necho \"\"\necho \"Then open:\"\necho \"  - http://localhost:8501 for the Streamlit interface\"\necho \"  - http://localhost:8080 to see the browser\""
  },
  {
    "path": "browser-use-demo/docker-compose.yml",
    "content": "services:\n  browser-use:\n    build:\n      context: .\n    image: browser-use-demo:latest\n    environment:\n      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}\n    volumes:\n      - ./validate_env.py:/home/browseruse/validate_env.py\n      - ${HOME}/.anthropic:/home/browseruse/.anthropic\n    ports:\n      - \"${VNC_PORT:-5900}:5900\"  # VNC\n      - \"${STREAMLIT_PORT:-8501}:8501\"  # Streamlit\n      - \"${NOVNC_PORT:-6080}:6080\"  # noVNC\n      - \"${HTTP_PORT:-8080}:8080\"  # HTTP server\n    stdin_open: true\n    tty: true\n    env_file:\n      - .env\n    develop:\n      watch:\n        - path: ./browser_use_demo\n          action: sync+restart\n          target: /home/browseruse/browser_use_demo\n        - path: ./validate_env.py\n          action: rebuild\n          target: /home/browseruse/validate_env.py"
  },
  {
    "path": "browser-use-demo/image/.config/tint2/applications/firefox-custom.desktop",
    "content": "[Desktop Entry]\nName=Firefox Custom\nComment=Open Firefox with custom URL\nExec=firefox-esr -new-window\nIcon=firefox-esr\nTerminal=false\nType=Application\nCategories=Network;WebBrowser;\n"
  },
  {
    "path": "browser-use-demo/image/.config/tint2/applications/gedit.desktop",
    "content": "[Desktop Entry]\nName=Gedit\nComment=Open gedit\nExec=gedit\nIcon=text-editor-symbolic\nTerminal=false\nType=Application\nCategories=TextEditor;\n"
  },
  {
    "path": "browser-use-demo/image/.config/tint2/applications/terminal.desktop",
    "content": "[Desktop Entry]\nName=Terminal\nComment=Open Terminal\nExec=xterm\nIcon=utilities-terminal\nTerminal=false\nType=Application\nCategories=System;TerminalEmulator;\n"
  },
  {
    "path": "browser-use-demo/image/.config/tint2/tint2rc",
    "content": "#-------------------------------------\n# Panel\npanel_items = TL\npanel_size = 100% 60\npanel_margin = 0 0\npanel_padding = 2 0 2\npanel_background_id = 1\nwm_menu = 0\npanel_dock = 0\npanel_position = bottom center horizontal\npanel_layer = top\npanel_monitor = all\npanel_shrink = 0\nautohide = 0\nautohide_show_timeout = 0\nautohide_hide_timeout = 0.5\nautohide_height = 2\nstrut_policy = follow_size\npanel_window_name = tint2\ndisable_transparency = 1\nmouse_effects = 1\nfont_shadow = 0\nmouse_hover_icon_asb = 100 0 10\nmouse_pressed_icon_asb = 100 0 0\nscale_relative_to_dpi = 0\nscale_relative_to_screen_height = 0\n\n#-------------------------------------\n# Taskbar\ntaskbar_mode = single_desktop\ntaskbar_hide_if_empty = 0\ntaskbar_padding = 0 0 2\ntaskbar_background_id = 0\ntaskbar_active_background_id = 0\ntaskbar_name = 1\ntaskbar_hide_inactive_tasks = 0\ntaskbar_hide_different_monitor = 0\ntaskbar_hide_different_desktop = 0\ntaskbar_always_show_all_desktop_tasks = 0\ntaskbar_name_padding = 4 2\ntaskbar_name_background_id = 0\ntaskbar_name_active_background_id = 0\ntaskbar_name_font_color = #e3e3e3 100\ntaskbar_name_active_font_color = #ffffff 100\ntaskbar_distribute_size = 0\ntaskbar_sort_order = none\ntask_align = left\n\n#-------------------------------------\n# Launcher\nlauncher_padding = 4 8 4\nlauncher_background_id = 0\nlauncher_icon_background_id = 0\nlauncher_icon_size = 48\nlauncher_icon_asb = 100 0 0\nlauncher_icon_theme_override = 0\nstartup_notifications = 1\nlauncher_tooltip = 1\n\n#-------------------------------------\n# Launcher icon\nlauncher_item_app = /usr/share/applications/libreoffice-calc.desktop\nlauncher_item_app = /home/computeruse/.config/tint2/applications/terminal.desktop\nlauncher_item_app = /home/computeruse/.config/tint2/applications/firefox-custom.desktop\nlauncher_item_app = /usr/share/applications/xpaint.desktop\nlauncher_item_app = /usr/share/applications/xpdf.desktop\nlauncher_item_app = /home/computeruse/.config/tint2/applications/gedit.desktop\nlauncher_item_app = /usr/share/applications/galculator.desktop\n\n#-------------------------------------\n# Background definitions\n# ID 1\nrounded = 0\nborder_width = 0\nbackground_color = #000000 60\nborder_color = #000000 30\n\n# ID 2\nrounded = 4\nborder_width = 1\nbackground_color = #777777 20\nborder_color = #777777 30\n\n# ID 3\nrounded = 4\nborder_width = 1\nbackground_color = #777777 20\nborder_color = #ffffff 40\n\n# ID 4\nrounded = 4\nborder_width = 1\nbackground_color = #aa4400 100\nborder_color = #aa7733 100\n\n# ID 5\nrounded = 4\nborder_width = 1\nbackground_color = #aaaa00 100\nborder_color = #aaaa00 100\n"
  },
  {
    "path": "browser-use-demo/image/entrypoint.sh",
    "content": "#!/bin/bash\nset -e\n\n# Validate environment configuration\necho \"Validating environment configuration...\"\npython validate_env.py\nif [ $? -ne 0 ]; then\n    echo \"Environment validation failed. Exiting.\"\n    exit 1\nfi\n\n./start_all.sh\n./novnc_startup.sh\n\npython http_server.py > /tmp/server_logs.txt 2>&1 &\n\nSTREAMLIT_SERVER_PORT=8501 python -m streamlit run browser_use_demo/streamlit.py > /tmp/streamlit_stdout.log &\n\necho \"✨ Browser Use Demo is ready!\"\necho \"➡️  Open http://localhost:8080 in your browser to begin\"\n\n# Keep the container running\ntail -f /dev/null\n"
  },
  {
    "path": "browser-use-demo/image/http_server.py",
    "content": "import os\nimport socket\nfrom http.server import HTTPServer, SimpleHTTPRequestHandler\n\n\nclass HTTPServerV6(HTTPServer):\n    address_family = socket.AF_INET6\n\n\ndef run_server():\n    os.chdir(os.path.dirname(__file__) + \"/static_content\")\n    server_address = (\"::\", 8080)\n    httpd = HTTPServerV6(server_address, SimpleHTTPRequestHandler)\n    print(\"Starting HTTP server on port 8080...\")  # noqa: T201\n    httpd.serve_forever()\n\n\nif __name__ == \"__main__\":\n    run_server()\n"
  },
  {
    "path": "browser-use-demo/image/index.html",
    "content": "<!doctype html>\n<html>\n    <head>\n        <title>Browser Use Demo</title>\n        <meta name=\"permissions-policy\" content=\"fullscreen=*\" />\n        <style>\n            body {\n                margin: 0;\n                padding: 0;\n                overflow: hidden;\n            }\n            .container {\n                display: flex;\n                height: 100vh;\n                width: 100vw;\n            }\n            .left {\n                flex: 1;\n                border: none;\n                height: 100vh;\n            }\n            .right {\n                flex: 2;\n                border: none;\n                height: 100vh;\n            }\n        </style>\n    </head>\n    <body>\n        <div class=\"container\">\n            <iframe\n                src=\"http://localhost:8501\"\n                class=\"left\"\n                allow=\"fullscreen\"\n            ></iframe>\n            <iframe\n                src=\"http://localhost:6080/vnc.html?view_only=1&autoconnect=1&resize=scale\"\n                class=\"right\"\n                allow=\"fullscreen\"\n            ></iframe>\n        </div>\n    </body>\n</html>\n"
  },
  {
    "path": "browser-use-demo/image/mutter_startup.sh",
    "content": "echo \"starting mutter\"\nXDG_SESSION_TYPE=x11 mutter --replace --sm-disable 2>/tmp/mutter_stderr.log &\n\n# Wait for tint2 window properties to appear\ntimeout=30\nwhile [ $timeout -gt 0 ]; do\n    if xdotool search --class \"mutter\" >/dev/null 2>&1; then\n        break\n    fi\n    sleep 1\n    ((timeout--))\ndone\n\nif [ $timeout -eq 0 ]; then\n    echo \"mutter stderr output:\" >&2\n    cat /tmp/mutter_stderr.log >&2\n    exit 1\nfi\n\nrm /tmp/mutter_stderr.log\n"
  },
  {
    "path": "browser-use-demo/image/novnc_startup.sh",
    "content": "#!/bin/bash\necho \"starting noVNC\"\n\n# Start noVNC with explicit websocket settings\n/opt/noVNC/utils/novnc_proxy \\\n    --vnc localhost:5900 \\\n    --listen 6080 \\\n    --web /opt/noVNC \\\n    > /tmp/novnc.log 2>&1 &\n\n# Wait for noVNC to start\ntimeout=10\nwhile [ $timeout -gt 0 ]; do\n    if netstat -tuln | grep -q \":6080 \"; then\n        break\n    fi\n    sleep 1\n    ((timeout--))\ndone\n\necho \"noVNC started successfully\"\n"
  },
  {
    "path": "browser-use-demo/image/start_all.sh",
    "content": "#!/bin/bash\n\nset -e\n\nexport DISPLAY=:${DISPLAY_NUM}\n./xvfb_startup.sh\n./tint2_startup.sh  # Keep taskbar for window management\n./mutter_startup.sh  # Keep window manager for controls\n./x11vnc_startup.sh\n"
  },
  {
    "path": "browser-use-demo/image/static_content/index.html",
    "content": "<!doctype html>\n<html>\n    <head>\n        <title>Browser Use Demo</title>\n        <meta name=\"permissions-policy\" content=\"fullscreen=*\" />\n        <style>\n            body {\n                margin: 0;\n                padding: 0;\n                overflow: hidden;\n            }\n            .container {\n                display: flex;\n                height: 100vh;\n                width: 100vw;\n            }\n            .left {\n                flex: 1;\n                border: none;\n                height: 100vh;\n            }\n            .right {\n                flex: 2;\n                border: none;\n                height: 100vh;\n            }\n        </style>\n    </head>\n    <body>\n        <div class=\"container\">\n            <iframe\n                src=\"http://localhost:8501\"\n                class=\"left\"\n                allow=\"fullscreen\"\n            ></iframe>\n            <iframe\n                id=\"vnc\"\n                src=\"http://127.0.0.1:6080/vnc.html?&resize=scale&autoconnect=1&view_only=1&reconnect=1&reconnect_delay=2000\"\n                class=\"right\"\n                allow=\"fullscreen\"\n            ></iframe>\n            <button\n                id=\"toggleViewOnly\"\n                style=\"position: absolute; top: 10px; right: 10px; z-index: 1000\"\n            >\n                Toggle Screen Control (Off)\n            </button>\n            <script>\n                document\n                    .getElementById(\"toggleViewOnly\")\n                    .addEventListener(\"click\", function () {\n                        var vncIframe = document.getElementById(\"vnc\");\n                        var button = document.getElementById(\"toggleViewOnly\");\n                        var currentSrc = vncIframe.src;\n                        if (currentSrc.includes(\"view_only=1\")) {\n                            vncIframe.src = currentSrc.replace(\n                                \"view_only=1\",\n                                \"view_only=0\",\n                            );\n                            button.innerText = \"Toggle Screen Control (On)\";\n                        } else {\n                            vncIframe.src = currentSrc.replace(\n                                \"view_only=0\",\n                                \"view_only=1\",\n                            );\n                            button.innerText = \"Toggle Screen Control (Off)\";\n                        }\n                    });\n            </script>\n        </div>\n    </body>\n</html>\n"
  },
  {
    "path": "browser-use-demo/image/tint2_startup.sh",
    "content": "#!/bin/bash\necho \"starting tint2 on display :$DISPLAY_NUM ...\"\n\n# Start tint2 and capture its stderr\ntint2 -c $HOME/.config/tint2/tint2rc 2>/tmp/tint2_stderr.log &\n\n# Wait for tint2 window properties to appear\ntimeout=30\nwhile [ $timeout -gt 0 ]; do\n    if xdotool search --class \"tint2\" >/dev/null 2>&1; then\n        break\n    fi\n    sleep 1\n    ((timeout--))\ndone\n\nif [ $timeout -eq 0 ]; then\n    echo \"tint2 stderr output:\" >&2\n    cat /tmp/tint2_stderr.log >&2\n    exit 1\nfi\n\n# Remove the temporary stderr log file\nrm /tmp/tint2_stderr.log\n"
  },
  {
    "path": "browser-use-demo/image/x11vnc_startup.sh",
    "content": "#!/bin/bash\necho \"starting vnc\"\n\n(x11vnc -display $DISPLAY \\\n    -forever \\\n    -shared \\\n    -wait 50 \\\n    -rfbport 5900 \\\n    -nopw \\\n    2>/tmp/x11vnc_stderr.log) &\n\nx11vnc_pid=$!\n\n# Wait for x11vnc to start\ntimeout=10\nwhile [ $timeout -gt 0 ]; do\n    if netstat -tuln | grep -q \":5900 \"; then\n        break\n    fi\n    sleep 1\n    ((timeout--))\ndone\n\nif [ $timeout -eq 0 ]; then\n    echo \"x11vnc failed to start, stderr output:\" >&2\n    cat /tmp/x11vnc_stderr.log >&2\n    exit 1\nfi\n\n: > /tmp/x11vnc_stderr.log\n\n# Monitor x11vnc process in the background\n(\n    while true; do\n        if ! kill -0 $x11vnc_pid 2>/dev/null; then\n            echo \"x11vnc process crashed, restarting...\" >&2\n            if [ -f /tmp/x11vnc_stderr.log ]; then\n                echo \"x11vnc stderr output:\" >&2\n                cat /tmp/x11vnc_stderr.log >&2\n                rm /tmp/x11vnc_stderr.log\n            fi\n            exec \"$0\"\n        fi\n        sleep 5\n    done\n) &\n"
  },
  {
    "path": "browser-use-demo/image/xvfb_startup.sh",
    "content": "#!/bin/bash\nset -e  # Exit on error\n\nDPI=96\nRES_AND_DEPTH=${WIDTH}x${HEIGHT}x24\n\n# Function to check if Xvfb is already running\ncheck_xvfb_running() {\n    if [ -e /tmp/.X${DISPLAY_NUM}-lock ]; then\n        return 0  # Xvfb is already running\n    else\n        return 1  # Xvfb is not running\n    fi\n}\n\n# Function to check if Xvfb is ready\nwait_for_xvfb() {\n    local timeout=10\n    local start_time=$(date +%s)\n    while ! xdpyinfo >/dev/null 2>&1; do\n        if [ $(($(date +%s) - start_time)) -gt $timeout ]; then\n            echo \"Xvfb failed to start within $timeout seconds\" >&2\n            return 1\n        fi\n        sleep 0.1\n    done\n    return 0\n}\n\n# Check if Xvfb is already running\nif check_xvfb_running; then\n    echo \"Lock file found for display ${DISPLAY}, checking if display is accessible...\"\n    # Verify that the display is actually accessible\n    if xdpyinfo >/dev/null 2>&1; then\n        echo \"Xvfb is already running and accessible on display ${DISPLAY}\"\n        exit 0\n    else\n        echo \"Display ${DISPLAY} is not accessible, cleaning up stale lock file...\"\n        rm -f /tmp/.X${DISPLAY_NUM}-lock\n        # Also clean up the socket if it exists\n        rm -f /tmp/.X11-unix/X${DISPLAY_NUM}\n    fi\nfi\n\n# Start Xvfb\nXvfb $DISPLAY -ac -screen 0 $RES_AND_DEPTH -retro -dpi $DPI -nolisten tcp -nolisten unix &\nXVFB_PID=$!\n\n# Wait for Xvfb to start\nif wait_for_xvfb; then\n    echo \"Xvfb started successfully on display ${DISPLAY}\"\n    echo \"Xvfb PID: $XVFB_PID\"\nelse\n    echo \"Xvfb failed to start\"\n    kill $XVFB_PID\n    exit 1\nfi\n"
  },
  {
    "path": "browser-use-demo/pytest.ini",
    "content": "[pytest]\n# Test discovery patterns\npython_files = test_*.py\npython_classes = Test*\npython_functions = test_*\n\n# Test directories\ntestpaths = tests\n\n# Markers for test categorization\nmarkers =\n    integration: Integration tests that test multiple components\n    slow: Tests that take longer than usual to run\n    asyncio: Tests that use asyncio\n\n# Coverage settings\naddopts =\n    -v\n    --strict-markers\n    --tb=short\n    --disable-warnings\n    -p pytest_asyncio\n\n# Asyncio configuration\nasyncio_mode = auto\nasyncio_default_fixture_loop_scope = function\n\n# Minimum Python version\nminversion = 3.11\n\n# Ignore specific warnings\nfilterwarnings =\n    ignore::DeprecationWarning\n    ignore::PendingDeprecationWarning"
  },
  {
    "path": "browser-use-demo/setup.py",
    "content": "from setuptools import setup, find_packages\n\nsetup(\n    name=\"browser-use-demo\",\n    version=\"0.1.0\",\n    packages=find_packages(),\n    python_requires=\">=3.11\",\n    install_requires=[\n        \"streamlit==1.41.0\",\n        \"anthropic[bedrock,vertex]>=0.39.0\",\n        \"jsonschema==4.22.0\",\n        \"boto3>=1.28.57\",\n        \"google-auth<3,>=2\",\n        \"playwright>=1.40.0\",\n    ],\n    extras_require={\n        \"test\": [\n            \"pytest==8.3.3\",\n            \"pytest-cov==4.1.0\",\n            \"pytest-mock==3.11.1\",\n            \"pytest-asyncio==0.23.6\",\n        ],\n        \"dev\": [\n            \"ruff==0.6.7\",\n            \"pyright>=1.1.300\",  \n            \"pre-commit==3.8.0\",\n        ],\n    },\n)\n"
  },
  {
    "path": "browser-use-demo/test-requirements.txt",
    "content": "pytest==8.3.3\npytest-cov==4.1.0\npytest-mock==3.11.1\npytest-asyncio==0.23.6"
  },
  {
    "path": "browser-use-demo/tests/README.md",
    "content": "# Browser Use Demo - Test Suite\n\nComprehensive test suite for the refactored Browser Use Demo with extensive edge case coverage.\n\n## Installation\n\n```bash\n# Install test dependencies\npip install -r test-requirements.txt\n\n# Or install with extras\npip install -e \".[test]\"\n```\n\n## Running Tests\n\n### Run all tests\n```bash\npytest tests/\n```\n\n### Run with coverage report\n```bash\npytest tests/ --cov=browser_tools_api_demo --cov-report=html\n# Open htmlcov/index.html to view coverage report\n```\n\n### Run specific test file\n```bash\npytest tests/test_message_renderer.py -v\n```\n\n### Run specific test class or method\n```bash\npytest tests/test_message_renderer.py::TestMessageRenderer -v\npytest tests/test_message_renderer.py::TestRenderMethod::test_render_string_message -v\n```\n\n### Run tests by marker\n```bash\n# Run only integration tests\npytest -m integration\n\n# Run tests excluding integration\npytest -m \"not integration\"\n\n# Run async tests\npytest -m asyncio\n```\n\n## Test Structure\n\n```\ntests/\n├── conftest.py                      # Shared fixtures and mocks\n├── test_message_renderer.py         # MessageRenderer class tests (~300 test cases)\n├── test_streamlit_helpers.py        # Helper function tests (~150 test cases)\n└── test_integration.py              # End-to-end integration tests (~50 test cases)\n```\n\n## Test Coverage\n\nThe test suite covers:\n\n### MessageRenderer (`test_message_renderer.py`)\n- Initialization with various state configurations\n- Rendering all message types (string, dict, ToolResult)\n- Conversation history rendering with complex structures\n- Edge cases: empty messages, None values, circular references\n- Error handling: malformed data, missing fields, exceptions\n- Unicode and special character handling\n- Performance with large messages\n\n### Streamlit Helpers (`test_streamlit_helpers.py`)\n- `setup_state()` with fresh and partial initialization\n- Environment variable handling (present/missing/invalid)\n- Lambda evaluation in state initialization\n- `get_or_create_event_loop()` with various loop states\n- `authenticate()` with different providers and key states\n- Concurrent access and thread safety\n- Error recovery scenarios\n\n### Integration Tests (`test_integration.py`)\n- Complete message rendering pipeline\n- State initialization and persistence\n- Event loop management with async operations\n- Error propagation across components\n- Full user interaction workflow\n- Performance with large datasets (1000+ messages)\n- Deeply nested content structures\n\n## Edge Cases Covered\n\n1. **Boundary Conditions**\n   - Empty strings, lists, dictionaries\n   - Single item collections\n   - Maximum size inputs (100k+ character messages)\n   - Null/None values\n\n2. **Type Mismatches**\n   - Wrong types for expected fields\n   - Missing required fields\n   - Extra unexpected fields\n   - Invalid message structures\n\n3. **State Inconsistencies**\n   - Tools referenced but not in session_state\n   - Partially initialized state\n   - Concurrent modifications\n   - Corrupted state\n\n4. **Error Conditions**\n   - Import errors\n   - Asyncio exceptions\n   - Environment variable errors\n   - Lambda evaluation failures\n   - Base64 decode errors\n\n5. **Performance Edge Cases**\n   - Very large message histories (1000+ messages)\n   - Deeply nested content (100+ levels)\n   - Circular references\n   - Unicode and special characters\n\n## Mocking Strategy\n\n### Streamlit Components\nAll Streamlit components are mocked to enable testing without a running Streamlit server:\n- `st.session_state`\n- `st.chat_message`\n- `st.markdown`, `st.write`, `st.error`, `st.code`, `st.image`\n- `st.chat_input`, `st.stop`\n\n### External Dependencies\n- `BrowserTool` - Mocked to avoid Playwright dependencies\n- `asyncio` event loops - Mocked for controlled testing\n- Environment variables - Mocked via `monkeypatch`\n\n## Fixtures\n\nKey fixtures provided in `conftest.py`:\n\n- `mock_streamlit` - Complete Streamlit mocking setup\n- `mock_browser_tool` - BrowserTool mock\n- `sample_tool_result` - Various ToolResult configurations\n- `sample_messages` - Diverse message structures for testing\n- `edge_case_messages` - Messages designed to trigger edge cases\n- `mock_asyncio_loop` - Controlled event loop for testing\n- `mock_environment` - Environment variable setup\n- `clean_environment` - Remove environment variables\n\n## Continuous Integration\n\nTo run tests in CI:\n\n```bash\n# Install dependencies\npip install -e \".[test]\"\n\n# Run tests with coverage\npytest tests/ --cov=browser_tools_api_demo --cov-report=xml --cov-report=term\n\n# Generate coverage badge\ncoverage-badge -o coverage.svg\n```\n\n## Contributing\n\nWhen adding new features or refactoring:\n1. Add corresponding tests for new functionality\n2. Ensure all edge cases are covered\n3. Run the full test suite before committing\n4. Maintain >95% code coverage\n5. Update this README if test structure changes"
  },
  {
    "path": "browser-use-demo/tests/__init__.py",
    "content": "\"\"\"Test suite for Browser Use Demo.\"\"\"\n"
  },
  {
    "path": "browser-use-demo/tests/conftest.py",
    "content": "\"\"\"Shared fixtures and configuration for tests.\"\"\"\n\nimport asyncio\nimport sys\nfrom pathlib import Path\nfrom unittest.mock import MagicMock, Mock, patch\n\nimport pytest\n\n# Add the parent directory to the path\nsys.path.insert(0, str(Path(__file__).parent.parent))\n\nfrom browser_use_demo.tools import ToolResult\n\n\n@pytest.fixture\ndef mock_streamlit():\n    \"\"\"Mock Streamlit module and session_state.\"\"\"\n    with patch(\"streamlit.session_state\") as mock_state:\n        # Initialize with default values\n        mock_state.hide_screenshots = False\n        mock_state.tools = {}\n        mock_state.messages = []\n        mock_state.api_key = \"test-key\"\n        mock_state.provider = MagicMock()\n        mock_state.event_loop = None\n\n        # Mock other streamlit components\n        with patch(\"streamlit.chat_message\") as mock_chat:\n            mock_chat.return_value.__enter__ = Mock()\n            mock_chat.return_value.__exit__ = Mock()\n\n            with patch(\"streamlit.markdown\") as mock_markdown:\n                with patch(\"streamlit.write\") as mock_write:\n                    with patch(\"streamlit.error\") as mock_error:\n                        with patch(\"streamlit.code\") as mock_code:\n                            with patch(\"streamlit.image\") as mock_image:\n                                yield {\n                                    \"session_state\": mock_state,\n                                    \"chat_message\": mock_chat,\n                                    \"markdown\": mock_markdown,\n                                    \"write\": mock_write,\n                                    \"error\": mock_error,\n                                    \"code\": mock_code,\n                                    \"image\": mock_image,\n                                }\n\n\n@pytest.fixture\ndef mock_browser_tool():\n    \"\"\"Mock BrowserTool to avoid Playwright dependencies.\"\"\"\n    with patch(\"browser_use_demo.tools.BrowserTool\") as mock_tool:\n        mock_instance = MagicMock()\n        mock_tool.return_value = mock_instance\n        yield mock_instance\n\n\n@pytest.fixture\ndef sample_tool_result():\n    \"\"\"Create sample ToolResult objects for testing.\"\"\"\n    return {\n        \"success\": ToolResult(output=\"Success message\"),\n        \"error\": ToolResult(error=\"Error message\"),\n        \"with_image\": ToolResult(\n            output=\"With screenshot\",\n            base64_image=\"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==\",\n        ),\n        \"empty\": ToolResult(),\n        \"all_fields\": ToolResult(\n            output=\"Output text\",\n            error=\"Error text\",\n            base64_image=\"base64data\",\n            system=\"System message\",\n        ),\n    }\n\n\n@pytest.fixture\ndef sample_messages():\n    \"\"\"Provide various message structures for testing edge cases.\"\"\"\n    return [\n        # Normal messages\n        {\"role\": \"user\", \"content\": \"Hello\"},\n        {\"role\": \"assistant\", \"content\": \"Hi there!\"},\n        # Complex content structures\n        {\n            \"role\": \"user\",\n            \"content\": [\n                {\"type\": \"text\", \"text\": \"Multiple items\"},\n                {\"type\": \"image\", \"source\": \"data:image/png;base64,abc123\"},\n            ],\n        },\n        # Assistant with tool results\n        {\n            \"role\": \"assistant\",\n            \"content\": [\n                {\"type\": \"text\", \"text\": \"Let me help with that\"},\n                {\n                    \"type\": \"tool_use\",\n                    \"name\": \"browser\",\n                    \"input\": {\"url\": \"example.com\"},\n                },\n                {\"type\": \"tool_result\", \"tool_use_id\": \"tool_123\"},\n            ],\n        },\n        # Edge cases\n        {\"role\": \"user\", \"content\": \"\"},  # Empty content\n        {\"role\": \"assistant\", \"content\": None},  # None content\n        {\"role\": \"unknown\", \"content\": \"Unknown role\"},  # Unknown role\n        {\"role\": \"user\"},  # Missing content field\n        {\"content\": \"No role\"},  # Missing role field\n        # Unicode and special characters\n        {\"role\": \"user\", \"content\": \"Hello 世界 🌍 \\n\\t\\r\"},\n        # Very long content\n        {\"role\": \"user\", \"content\": \"x\" * 10000},\n        # Nested structures\n        {\n            \"role\": \"assistant\",\n            \"content\": [\n                {\n                    \"type\": \"text\",\n                    \"text\": \"Nested\",\n                    \"metadata\": {\"nested\": {\"deeply\": {\"value\": 123}}},\n                }\n            ],\n        },\n    ]\n\n\n@pytest.fixture\ndef edge_case_messages():\n    \"\"\"Messages specifically designed to test edge cases and error conditions.\"\"\"\n    return {\n        \"empty_list\": [],\n        \"none\": None,\n        \"malformed_dict\": {\"not\": \"valid\", \"message\": \"structure\"},\n        \"circular_ref\": _create_circular_reference(),\n        \"missing_tool\": {\n            \"role\": \"assistant\",\n            \"content\": [{\"type\": \"tool_result\", \"tool_use_id\": \"nonexistent_tool\"}],\n        },\n        \"invalid_types\": [\n            {\"role\": 123, \"content\": \"Invalid role type\"},\n            {\"role\": \"user\", \"content\": {\"invalid\": \"content structure\"}},\n        ],\n        \"huge_message\": {\n            \"role\": \"user\",\n            \"content\": [{\"type\": \"text\", \"text\": \"x\" * 1000000}],\n        },\n    }\n\n\ndef _create_circular_reference():\n    \"\"\"Helper to create a message with circular reference.\"\"\"\n    msg = {\"role\": \"user\", \"content\": []}\n    msg[\"content\"].append(msg)  # Circular reference\n    return msg\n\n\n@pytest.fixture\ndef mock_asyncio_loop():\n    \"\"\"Mock asyncio event loop for testing.\"\"\"\n    loop = Mock(spec=asyncio.AbstractEventLoop)\n    loop.is_closed.return_value = False\n    loop.run_until_complete = Mock(side_effect=lambda coro: asyncio.run(coro))\n    return loop\n\n\n@pytest.fixture\ndef mock_environment(monkeypatch):\n    \"\"\"Mock environment variables for testing.\"\"\"\n    env_vars = {\n        \"ANTHROPIC_API_KEY\": \"test-api-key\",\n    }\n\n    for key, value in env_vars.items():\n        monkeypatch.setenv(key, value)\n\n    return env_vars\n\n\n@pytest.fixture\ndef clean_environment(monkeypatch):\n    \"\"\"Remove environment variables for testing missing env scenarios.\"\"\"\n    keys_to_remove = [\n        \"ANTHROPIC_API_KEY\",\n    ]\n\n    for key in keys_to_remove:\n        monkeypatch.delenv(key, raising=False)\n\n    return keys_to_remove\n\n\n@pytest.fixture\ndef mock_provider():\n    \"\"\"Mock APIProvider enum.\"\"\"\n    with patch(\"browser_use_demo.loop.APIProvider\") as mock:\n        mock.ANTHROPIC = \"anthropic\"\n        mock.BEDROCK = \"bedrock\"\n        mock.VERTEX = \"vertex\"\n        yield mock\n\n\n@pytest.fixture\ndef mock_api_response_with_text_and_tools():\n    \"\"\"Mock API response containing both text and tool uses.\"\"\"\n    response = Mock()\n    response.content = [\n        Mock(type=\"text\", text=\"I'll help you with that task\"),\n        Mock(\n            type=\"tool_use\",\n            id=\"tool_001\",\n            name=\"browser\",\n            input={\"action\": \"screenshot\"}\n        ),\n        Mock(type=\"text\", text=\"Here's what I found\"),\n        Mock(\n            type=\"tool_use\",\n            id=\"tool_002\",\n            name=\"browser\",\n            input={\"action\": \"navigate\", \"url\": \"example.com\"}\n        )\n    ]\n    return response\n\n\n@pytest.fixture\ndef mock_tool_collection():\n    \"\"\"Mock ToolCollection for testing.\"\"\"\n    from unittest.mock import AsyncMock\n\n    mock_browser = AsyncMock()\n    mock_browser.return_value = ToolResult(output=\"Tool executed successfully\")\n\n    collection = Mock()\n    collection.tool_map = {\"browser\": mock_browser}\n    collection.to_params = Mock(return_value=[\n        {\n            \"name\": \"browser\",\n            \"description\": \"Browser automation tool\",\n            \"input_schema\": {}\n        }\n    ])\n\n    return collection\n\n\n@pytest.fixture\ndef sample_mixed_content_messages():\n    \"\"\"Sample messages with mixed text and tool content.\"\"\"\n    return [\n        {\n            \"role\": \"user\",\n            \"content\": \"Can you help me navigate and take a screenshot?\"\n        },\n        {\n            \"role\": \"assistant\",\n            \"content\": [\n                {\"type\": \"text\", \"text\": \"Sure, I'll navigate to the page first\"},\n                {\n                    \"type\": \"tool_use\",\n                    \"id\": \"tool_nav\",\n                    \"name\": \"browser\",\n                    \"input\": {\"action\": \"navigate\", \"url\": \"example.com\"}\n                },\n                {\"type\": \"text\", \"text\": \"Now taking a screenshot\"},\n                {\n                    \"type\": \"tool_use\",\n                    \"id\": \"tool_shot\",\n                    \"name\": \"browser\",\n                    \"input\": {\"action\": \"screenshot\"}\n                }\n            ]\n        },\n        {\n            \"role\": \"user\",\n            \"content\": [\n                {\n                    \"type\": \"tool_result\",\n                    \"tool_use_id\": \"tool_nav\",\n                    \"content\": [{\"type\": \"text\", \"text\": \"Navigation successful\"}]\n                },\n                {\n                    \"type\": \"tool_result\",\n                    \"tool_use_id\": \"tool_shot\",\n                    \"content\": [\n                        {\"type\": \"text\", \"text\": \"Screenshot taken\"},\n                        {\n                            \"type\": \"image\",\n                            \"source\": {\n                                \"type\": \"base64\",\n                                \"media_type\": \"image/png\",\n                                \"data\": \"base64imagedata\"\n                            }\n                        }\n                    ]\n                }\n            ]\n        }\n    ]\n"
  },
  {
    "path": "browser-use-demo/tests/test_integration.py",
    "content": "\"\"\"Integration tests for the refactored Browser Use Demo.\"\"\"\n\nfrom unittest.mock import AsyncMock, MagicMock, Mock, patch\n\nimport pytest\nfrom browser_use_demo.loop import APIProvider\nfrom browser_use_demo.message_renderer import MessageRenderer\nfrom browser_use_demo.streamlit import (\n    get_or_create_event_loop,\n    setup_state,\n)\nfrom browser_use_demo.tools import ToolResult\n\n\n@pytest.mark.integration\nclass TestFullMessageRenderingPipeline:\n    \"\"\"Test complete message rendering pipeline.\"\"\"\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    @patch(\"streamlit.chat_message\")\n    @patch(\"streamlit.markdown\")\n    @patch(\"streamlit.write\")\n    @patch(\"streamlit.error\")\n    def test_full_conversation_rendering(\n        self, mock_error, mock_write, mock_markdown, mock_chat, mock_state\n    ):\n        \"\"\"Test rendering a complete conversation with various message types.\"\"\"\n\n        # Setup mock state\n        mock_state.hide_screenshots = False\n        mock_state.tools = {\n            \"tool_1\": ToolResult(output=\"Tool output 1\"),\n            \"tool_2\": ToolResult(error=\"Tool error 2\"),\n        }\n\n        # Create complex conversation\n        messages = [\n            {\"role\": \"user\", \"content\": \"Hello, can you help me?\"},\n            {\n                \"role\": \"assistant\",\n                \"content\": [\n                    {\"type\": \"text\", \"text\": \"Sure, let me help you.\"},\n                    {\n                        \"type\": \"tool_use\",\n                        \"name\": \"browser\",\n                        \"input\": {\"url\": \"example.com\"},\n                    },\n                    {\"type\": \"tool_result\", \"tool_use_id\": \"tool_1\"},\n                ],\n            },\n            {\"role\": \"user\", \"content\": [{\"type\": \"text\", \"text\": \"Thank you!\"}]},\n            {\n                \"role\": \"assistant\",\n                \"content\": [\n                    {\"type\": \"text\", \"text\": \"You're welcome!\"},\n                    {\"type\": \"tool_result\", \"tool_use_id\": \"tool_2\"},\n                ],\n            },\n        ]\n\n        # Mock chat_message context manager\n        mock_chat.return_value.__enter__ = Mock()\n        mock_chat.return_value.__exit__ = Mock()\n\n        # Render full conversation\n        renderer = MessageRenderer(mock_state)\n        renderer.render_conversation_history(messages)\n\n        # Verify all message types were rendered\n        assert mock_markdown.call_count >= 3  # Text messages\n        assert mock_write.call_count >= 2  # Tool use and text blocks\n        assert mock_error.call_count == 1  # Tool error\n\n\n@pytest.mark.integration\nclass TestStateInitializationAndPersistence:\n    \"\"\"Test state initialization and persistence across operations.\"\"\"\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    @patch(\"browser_use_demo.tools.BrowserTool\")\n    def test_complete_state_initialization(self, mock_browser_tool, mock_state):\n        \"\"\"Test complete state initialization flow.\"\"\"\n\n        # Simulate fresh state\n        initialized_keys = set()\n\n        def setitem_side_effect(key, value):\n            initialized_keys.add(key)\n            setattr(mock_state, key, value)\n\n        mock_state.__contains__.return_value = False\n        mock_state.__setitem__.side_effect = setitem_side_effect\n\n        # Initialize state\n        setup_state()\n\n        # Verify all required keys were initialized\n        expected_keys = {\n            \"messages\",\n            \"api_key\",\n            \"provider\",\n            \"model\",\n            \"max_tokens\",\n            \"system_prompt\",\n            \"hide_screenshots\",\n            \"tools\",\n            \"browser_tool\",\n            \"event_loop\",\n            \"rendered_message_count\",\n            \"is_agent_running\",\n            \"active_messages\",\n            \"active_response_container\",\n        }\n\n        assert expected_keys.issubset(initialized_keys)\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    def test_state_persistence_across_renders(self, mock_state):\n        \"\"\"Test that state persists across multiple render calls.\"\"\"\n\n        # Initialize state\n        mock_state.tools = {\"tool_1\": ToolResult(output=\"Persistent tool\")}\n        mock_state.messages = [{\"role\": \"user\", \"content\": \"Initial message\"}]\n\n        # Create renderer and render\n        renderer1 = MessageRenderer(mock_state)\n        with patch(\"streamlit.chat_message\"), patch(\"streamlit.markdown\"):\n            renderer1.render_conversation_history(mock_state.messages)\n\n        # Add more messages\n        mock_state.messages.append({\"role\": \"assistant\", \"content\": \"Response\"})\n\n        # Create new renderer instance and render again\n        renderer2 = MessageRenderer(mock_state)\n        with patch(\"streamlit.chat_message\"), patch(\"streamlit.markdown\") as mock_md:\n            renderer2.render_conversation_history(mock_state.messages)\n\n            # Should render both messages\n            assert mock_md.call_count >= 2\n\n\n@pytest.mark.integration\nclass TestEventLoopManagementWithAsync:\n    \"\"\"Test event loop management with async operations.\"\"\"\n\n    def test_async_agent_execution(self):\n        \"\"\"Test running async agent with event loop management.\"\"\"\n\n        with patch(\"streamlit.session_state\", new_callable=MagicMock) as mock_state:\n            mock_state.event_loop = None\n\n            with patch(\"asyncio.set_event_loop\"):\n                with patch(\"asyncio.new_event_loop\") as mock_new_loop:\n                    mock_loop = MagicMock()\n                    mock_loop.is_closed.return_value = False\n                    mock_new_loop.return_value = mock_loop\n\n                    loop = get_or_create_event_loop()\n\n                    # Verify loop was created and set\n                    assert loop == mock_loop\n                    assert mock_state.event_loop == mock_loop\n\n            # Test that the loop can handle async operations (mocked)\n            async def mock_agent(input_text):\n                return f\"Processed: {input_text}\"\n\n            # Mock running the async function\n            mock_loop.run_until_complete = MagicMock(\n                return_value=\"Processed: Test input\"\n            )\n            result = mock_loop.run_until_complete(mock_agent(\"Test input\"))\n            assert result == \"Processed: Test input\"\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    def test_concurrent_async_operations(self, mock_state):\n        \"\"\"Test handling concurrent async operations.\"\"\"\n\n        mock_state.event_loop = None\n\n        with patch(\"asyncio.set_event_loop\"):\n            with patch(\"asyncio.new_event_loop\") as mock_new_loop:\n                mock_loop = MagicMock()\n                mock_loop.is_closed.return_value = False\n                mock_new_loop.return_value = mock_loop\n\n                loop = get_or_create_event_loop()\n\n        # Simulate concurrent operations\n        async def async_task(task_id):\n            return f\"Task {task_id} complete\"\n\n        # Mock gather to simulate concurrent execution\n        expected_results = [f\"Task {i} complete\" for i in range(5)]\n\n        with patch(\"asyncio.gather\") as mock_gather:\n            mock_gather.return_value = expected_results\n\n            # Test that multiple tasks can be handled\n            tasks = [async_task(i) for i in range(5)]\n            results = mock_gather(*tasks)\n\n            assert len(results) == 5\n            assert all(\"complete\" in r for r in results)\n            mock_gather.assert_called_once()\n\n\n@pytest.mark.integration\nclass TestErrorPropagationAndHandling:\n    \"\"\"Test error propagation and handling across the system.\"\"\"\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    @patch(\"streamlit.error\")\n    def test_rendering_error_propagation(self, mock_error, mock_state):\n        \"\"\"Test that rendering errors are properly propagated.\"\"\"\n\n        mock_state.tools = {}\n\n        # Create message that will cause error\n        messages = [\n            {\n                \"role\": \"assistant\",\n                \"content\": [{\"type\": \"tool_result\", \"tool_use_id\": \"nonexistent\"}],\n            }\n        ]\n\n        renderer = MessageRenderer(mock_state)\n        with patch(\"streamlit.chat_message\"):\n            # Should handle missing tool gracefully\n            renderer.render_conversation_history(messages)\n\n        # Error should not be called for missing tool (handled gracefully)\n        mock_error.assert_not_called()\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    def test_initialization_error_recovery(self, mock_state):\n        \"\"\"Test recovery from initialization errors.\"\"\"\n\n        mock_state.__contains__.return_value = False\n        # Set provider to valid enum value so lambda can access it\n        mock_state.provider = APIProvider.ANTHROPIC\n\n        # First call fails\n        with patch(\"browser_use_demo.tools.BrowserTool\") as mock_browser:\n            mock_browser.side_effect = [Exception(\"Init failed\"), MagicMock()]\n\n            # First attempt should fail\n            with pytest.raises(Exception, match=\"Init failed\"):\n                setup_state()\n\n            # Second attempt should succeed\n            setup_state()\n            assert mock_browser.call_count == 2\n\n\n@pytest.mark.integration\nclass TestCompleteWorkflow:\n    \"\"\"Test complete workflow from initialization to rendering.\"\"\"\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    @patch(\"streamlit.chat_input\")\n    @patch(\"streamlit.chat_message\")\n    @patch(\"streamlit.markdown\")\n    @patch(\"browser_use_demo.tools.BrowserTool\")\n    @patch(\"browser_use_demo.streamlit.run_agent\", new_callable=AsyncMock)\n    def test_complete_user_interaction_flow(\n        self,\n        mock_run_agent,\n        mock_browser_tool,\n        mock_markdown,\n        mock_chat_message,\n        mock_chat_input,\n        mock_state,\n    ):\n        \"\"\"Test complete flow from user input to message rendering.\"\"\"\n\n        # Setup initial state\n        mock_state.__contains__.return_value = False\n        # Set provider to valid enum value so lambda can access it\n        mock_state.provider = APIProvider.ANTHROPIC\n        setup_state()\n\n        # Simulate user input\n        user_input = \"Browse to example.com\"\n        mock_chat_input.return_value = user_input\n\n        # Setup event loop\n        with patch(\"asyncio.new_event_loop\") as mock_new_loop:\n            mock_loop = MagicMock()\n            mock_loop.is_closed.return_value = False\n            mock_loop.run_until_complete = MagicMock()\n            mock_new_loop.return_value = mock_loop\n\n            with patch(\"asyncio.set_event_loop\"):\n                loop = get_or_create_event_loop()\n\n        # Simulate agent response\n        mock_run_agent.return_value = None\n\n        # Mock chat message context\n        mock_chat_message.return_value.__enter__ = Mock()\n        mock_chat_message.return_value.__exit__ = Mock()\n\n        # Simulate the workflow\n        # User provides input\n        if user_input:\n            loop.run_until_complete(mock_run_agent(user_input))\n\n            mock_loop.run_until_complete.assert_called_once()\n            mock_run_agent.assert_called_once_with(user_input)\n\n\n@pytest.mark.integration\nclass TestPerformanceAndScalability:\n    \"\"\"Test performance with large datasets and edge cases.\"\"\"\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    @patch(\"streamlit.chat_message\")\n    @patch(\"streamlit.markdown\")\n    def test_large_conversation_history(\n        self, mock_markdown, mock_chat_message, mock_state\n    ):\n        \"\"\"Test rendering very large conversation history.\"\"\"\n\n        # Create large conversation (1000 messages)\n        large_conversation = []\n        for i in range(1000):\n            role = \"user\" if i % 2 == 0 else \"assistant\"\n            large_conversation.append({\"role\": role, \"content\": f\"Message {i}\"})\n\n        mock_state.tools = {}\n        mock_chat_message.return_value.__enter__ = Mock()\n        mock_chat_message.return_value.__exit__ = Mock()\n\n        renderer = MessageRenderer(mock_state)\n\n        # Should handle large conversation without issues\n        renderer.render_conversation_history(large_conversation)\n\n        # Verify all messages were processed\n        assert mock_markdown.call_count == 1000\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    def test_deeply_nested_content_performance(self, mock_state):\n        \"\"\"Test performance with deeply nested content structures.\"\"\"\n\n        # Create deeply nested structure\n        content = {\"type\": \"text\", \"text\": \"Base\"}\n        for i in range(100):\n            content = {\"type\": \"wrapper\", \"content\": [content], \"depth\": i}\n\n        messages = [{\"role\": \"user\", \"content\": [content]}]\n\n        mock_state.tools = {}\n\n        with patch(\"streamlit.chat_message\"), patch(\"streamlit.write\"):\n            renderer = MessageRenderer(mock_state)\n\n            # Should handle deep nesting without stack overflow\n            renderer.render_conversation_history(messages)\n"
  },
  {
    "path": "browser-use-demo/tests/test_message_renderer.py",
    "content": "\"\"\"Tests for MessageRenderer class with comprehensive edge case coverage.\"\"\"\n\nfrom unittest.mock import MagicMock, Mock, patch\n\nimport pytest\nfrom browser_use_demo.message_renderer import MessageRenderer, Sender\nfrom browser_use_demo.tools import ToolResult\n\n\nclass TestMessageRenderer:\n    \"\"\"Test suite for MessageRenderer class.\"\"\"\n\n    def test_initialization(self, mock_streamlit):\n        \"\"\"Test MessageRenderer initialization.\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n        assert renderer.session_state == mock_streamlit[\"session_state\"]\n\n    def test_initialization_with_none_state(self):\n        \"\"\"Test initialization with None session state.\"\"\"\n        renderer = MessageRenderer(None)\n        assert renderer.session_state is None\n\n    def test_initialization_with_empty_state(self):\n        \"\"\"Test initialization with empty session state.\"\"\"\n        empty_state = MagicMock()\n        renderer = MessageRenderer(empty_state)\n        assert renderer.session_state == empty_state\n\n\nclass TestRenderMethod:\n    \"\"\"Test the main render method with various inputs.\"\"\"\n\n    def test_render_string_message(self, mock_streamlit):\n        \"\"\"Test rendering a simple string message.\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n        renderer.render(Sender.USER, \"Hello world\")\n\n        mock_streamlit[\"chat_message\"].assert_called_with(Sender.USER)\n        mock_streamlit[\"markdown\"].assert_called_with(\"Hello world\")\n\n    def test_render_empty_string(self, mock_streamlit):\n        \"\"\"Test rendering an empty string (should skip).\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n        renderer.render(Sender.USER, \"\")\n\n        mock_streamlit[\"chat_message\"].assert_not_called()\n\n    def test_render_none_message(self, mock_streamlit):\n        \"\"\"Test rendering None message (should skip).\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n        renderer.render(Sender.BOT, None)\n\n        mock_streamlit[\"chat_message\"].assert_not_called()\n\n    def test_render_tool_result_with_output(self, mock_streamlit, sample_tool_result):\n        \"\"\"Test rendering ToolResult with output.\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n        renderer.render(Sender.TOOL, sample_tool_result[\"success\"])\n\n        mock_streamlit[\"markdown\"].assert_called_with(\"Success message\")\n\n    def test_render_tool_result_with_error(self, mock_streamlit, sample_tool_result):\n        \"\"\"Test rendering ToolResult with error.\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n        renderer.render(Sender.TOOL, sample_tool_result[\"error\"])\n\n        mock_streamlit[\"error\"].assert_called_with(\"Error message\")\n\n    def test_render_tool_result_with_image(self, mock_streamlit, sample_tool_result):\n        \"\"\"Test rendering ToolResult with image.\"\"\"\n        mock_streamlit[\"session_state\"].hide_screenshots = False\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n        renderer.render(Sender.TOOL, sample_tool_result[\"with_image\"])\n\n        mock_streamlit[\"markdown\"].assert_called_with(\"With screenshot\")\n        # Image should be decoded and displayed\n        assert mock_streamlit[\"image\"].called\n\n    def test_render_tool_result_with_hidden_screenshots(\n        self, mock_streamlit, sample_tool_result\n    ):\n        \"\"\"Test that images are hidden when hide_screenshots is True.\"\"\"\n        mock_streamlit[\"session_state\"].hide_screenshots = True\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n        renderer.render(Sender.TOOL, sample_tool_result[\"with_image\"])\n\n        # Should render text but not image\n        mock_streamlit[\"markdown\"].assert_called_with(\"With screenshot\")\n        mock_streamlit[\"image\"].assert_not_called()\n\n    def test_render_dict_message_text_type(self, mock_streamlit):\n        \"\"\"Test rendering dictionary message with text type.\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n        message = {\"type\": \"text\", \"text\": \"Hello from dict\"}\n        renderer.render(Sender.USER, message)\n\n        mock_streamlit[\"write\"].assert_called_with(\"Hello from dict\")\n\n    def test_render_dict_message_tool_use_type(self, mock_streamlit):\n        \"\"\"Test rendering dictionary message with tool_use type.\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n        message = {\n            \"type\": \"tool_use\",\n            \"name\": \"browser_tool\",\n            \"input\": {\"url\": \"example.com\"},\n        }\n        renderer.render(Sender.BOT, message)\n\n        expected_code = \"Tool Use: browser_tool\\nInput: {'url': 'example.com'}\"\n        mock_streamlit[\"code\"].assert_called_with(expected_code)\n\n    def test_render_dict_message_unknown_type(self, mock_streamlit):\n        \"\"\"Test rendering dictionary message with unknown type.\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n        message = {\"type\": \"unknown\", \"data\": \"some data\"}\n        renderer.render(Sender.BOT, message)\n\n        # Should fall back to generic write\n        mock_streamlit[\"write\"].assert_called_with(message)\n\n    def test_render_very_long_message(self, mock_streamlit):\n        \"\"\"Test rendering extremely long messages.\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n        long_message = \"x\" * 100000  # 100k characters\n        renderer.render(Sender.USER, long_message)\n\n        mock_streamlit[\"markdown\"].assert_called_with(long_message)\n\n    def test_render_unicode_special_chars(self, mock_streamlit):\n        \"\"\"Test rendering messages with unicode and special characters.\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n        special_message = \"Hello 世界 🌍 \\n\\t\\r ñáéíóú\"\n        renderer.render(Sender.USER, special_message)\n\n        mock_streamlit[\"markdown\"].assert_called_with(special_message)\n\n\nclass TestConversationHistory:\n    \"\"\"Test render_conversation_history method with various scenarios.\"\"\"\n\n    def test_render_empty_history(self, mock_streamlit):\n        \"\"\"Test rendering empty conversation history.\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n        renderer.render_conversation_history([])\n\n        # No rendering should occur\n        mock_streamlit[\"chat_message\"].assert_not_called()\n\n    def test_render_single_message(self, mock_streamlit):\n        \"\"\"Test rendering single message in history.\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n        messages = [{\"role\": \"user\", \"content\": \"Hello\"}]\n        renderer.render_conversation_history(messages)\n\n        mock_streamlit[\"markdown\"].assert_called_with(\"Hello\")\n\n    def test_render_multiple_messages(self, mock_streamlit, sample_messages):\n        \"\"\"Test rendering multiple messages with different roles.\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n        renderer.render_conversation_history(sample_messages[:2])\n\n        # Should render both messages\n        assert mock_streamlit[\"markdown\"].call_count >= 2\n\n    def test_render_unknown_role(self, mock_streamlit):\n        \"\"\"Test handling messages with unknown roles.\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n        messages = [{\"role\": \"unknown_role\", \"content\": \"Test\"}]\n        renderer.render_conversation_history(messages)\n\n        # Should not crash, but won't render\n        mock_streamlit[\"markdown\"].assert_not_called()\n\n    def test_render_missing_content_field(self, mock_streamlit):\n        \"\"\"Test handling messages missing content field.\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n        messages = [{\"role\": \"user\"}]  # Missing content\n\n        # Should not crash - will get KeyError but handler should manage it gracefully\n        try:\n            renderer.render_conversation_history(messages)\n        except KeyError:\n            pass  # Expected when content field is missing\n\n    def test_render_none_content(self, mock_streamlit):\n        \"\"\"Test handling messages with None content.\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n        messages = [{\"role\": \"user\", \"content\": None}]\n        renderer.render_conversation_history(messages)\n\n        # Should handle gracefully without rendering\n        mock_streamlit[\"markdown\"].assert_not_called()\n\n    def test_render_list_content(self, mock_streamlit):\n        \"\"\"Test rendering messages with list content.\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n        messages = [\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\"type\": \"text\", \"text\": \"First\"},\n                    {\"type\": \"text\", \"text\": \"Second\"},\n                ],\n            }\n        ]\n        renderer.render_conversation_history(messages)\n\n        # Should render both text blocks\n        calls = mock_streamlit[\"markdown\"].call_args_list\n        assert any(\"First\" in str(call) for call in calls)\n        assert any(\"Second\" in str(call) for call in calls)\n\n    def test_skip_image_blocks_in_history(self, mock_streamlit):\n        \"\"\"Test that image blocks are skipped in conversation history.\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n        messages = [\n            {\n                \"role\": \"user\",\n                \"content\": [\n                    {\"type\": \"text\", \"text\": \"Text message\"},\n                    {\"type\": \"image\", \"source\": \"data:image/png;base64,abc\"},\n                ],\n            }\n        ]\n        renderer.render_conversation_history(messages)\n\n        # Should only render text, not image\n        mock_streamlit[\"markdown\"].assert_called_with(\"Text message\")\n        mock_streamlit[\"image\"].assert_not_called()\n\n    def test_tool_result_in_assistant_message(self, mock_streamlit, sample_tool_result):\n        \"\"\"Test rendering tool results from assistant messages.\"\"\"\n        mock_streamlit[\"session_state\"].tools = {\n            \"tool_123\": sample_tool_result[\"success\"]\n        }\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n        messages = [\n            {\n                \"role\": \"assistant\",\n                \"content\": [{\"type\": \"tool_result\", \"tool_use_id\": \"tool_123\"}],\n            }\n        ]\n        renderer.render_conversation_history(messages)\n\n        # Should render the tool result from session state\n        mock_streamlit[\"markdown\"].assert_called_with(\"Success message\")\n\n    def test_missing_tool_in_session_state(self, mock_streamlit):\n        \"\"\"Test handling tool_use_id that doesn't exist in session state.\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n        messages = [\n            {\n                \"role\": \"assistant\",\n                \"content\": [{\"type\": \"tool_result\", \"tool_use_id\": \"nonexistent\"}],\n            }\n        ]\n        renderer.render_conversation_history(messages)\n\n        # Should handle gracefully without crashing\n        mock_streamlit[\"markdown\"].assert_not_called()\n\n\nclass TestEdgeCases:\n    \"\"\"Test edge cases and error conditions.\"\"\"\n\n    def test_circular_reference_handling(self, mock_streamlit):\n        \"\"\"Test handling circular references in messages.\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n\n        # Create circular reference\n        content = []\n        content.append({\"type\": \"text\", \"text\": \"Normal\", \"ref\": content})\n        messages = [{\"role\": \"user\", \"content\": content}]\n\n        # Should not crash or infinite loop\n        renderer.render_conversation_history(messages)\n\n    def test_malformed_tool_result(self, mock_streamlit):\n        \"\"\"Test handling malformed ToolResult objects.\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n\n        # Create a mock that doesn't have expected attributes\n        malformed = Mock(spec=[])  # No attributes\n        renderer.render(Sender.TOOL, malformed)\n\n        # Should handle gracefully\n        mock_streamlit[\"markdown\"].assert_not_called()\n\n    def test_exception_in_rendering(self, mock_streamlit):\n        \"\"\"Test that exceptions during rendering are propagated.\"\"\"\n        # Setup the chat_message context manager properly\n        mock_chat_cm = MagicMock()\n        mock_chat_cm.__enter__ = Mock(return_value=None)\n        mock_chat_cm.__exit__ = Mock(return_value=None)\n        mock_streamlit[\"chat_message\"].return_value = mock_chat_cm\n\n        # Set markdown to raise an exception\n        mock_streamlit[\"markdown\"].side_effect = Exception(\"Render error\")\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n\n        # Should let the exception propagate for markdown rendering\n        with pytest.raises(Exception, match=\"Render error\"):\n            renderer.render(Sender.USER, \"Test message\")\n\n    def test_normalize_content_with_various_inputs(self, mock_streamlit):\n        \"\"\"Test _normalize_content with various input types.\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n\n        # String input\n        assert renderer._normalize_content(\"test\") == [\"test\"]\n\n        # List input\n        assert renderer._normalize_content([1, 2, 3]) == [1, 2, 3]\n\n        # None input\n        assert renderer._normalize_content(None) == [None]\n\n        # Dict input\n        assert renderer._normalize_content({\"key\": \"value\"}) == [{\"key\": \"value\"}]\n\n    def test_deeply_nested_content(self, mock_streamlit):\n        \"\"\"Test handling deeply nested content structures.\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n\n        # Create deeply nested structure\n        nested = {\"type\": \"text\", \"text\": \"Deep\"}\n        for _ in range(100):\n            nested = {\"type\": \"wrapper\", \"content\": nested}\n\n        messages = [{\"role\": \"user\", \"content\": [nested]}]\n        renderer.render_conversation_history(messages)\n\n        # Should handle without stack overflow\n\n    def test_concurrent_modification(self, mock_streamlit):\n        \"\"\"Test behavior when session state is modified during rendering.\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n\n        # Setup mock properly for context manager\n        mock_cm = MagicMock()\n        mock_cm.__enter__ = Mock(return_value=None)\n        mock_cm.__exit__ = Mock(return_value=None)\n\n        # Simulate modification during rendering\n        def modify_state(*args, **kwargs):\n            mock_streamlit[\"session_state\"].tools = {}\n            return mock_cm\n\n        mock_streamlit[\"chat_message\"].side_effect = modify_state\n\n        # Should complete rendering despite modifications\n        renderer.render(Sender.USER, \"Test\")\n\n    def test_invalid_sender_type(self, mock_streamlit):\n        \"\"\"Test handling invalid sender types.\"\"\"\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n\n        # Use invalid sender\n        renderer.render(\"invalid_sender\", \"Message\")\n\n        # Should still render with the provided sender\n        mock_streamlit[\"chat_message\"].assert_called_with(\"invalid_sender\")\n\n    def test_base64_decode_error(self, mock_streamlit):\n        \"\"\"Test handling invalid base64 image data.\"\"\"\n        # Setup the chat_message context manager properly\n        mock_chat_cm = MagicMock()\n        mock_chat_cm.__enter__ = Mock(return_value=None)\n        mock_chat_cm.__exit__ = Mock(return_value=None)\n        mock_streamlit[\"chat_message\"].return_value = mock_chat_cm\n\n        # Setup session state to not hide screenshots\n        mock_streamlit[\"session_state\"].hide_screenshots = False\n\n        renderer = MessageRenderer(mock_streamlit[\"session_state\"])\n        tool_result = ToolResult(\n            output=\"With bad image\", base64_image=\"invalid_base64_!@#$\"\n        )\n\n        with patch(\"base64.b64decode\") as mock_decode:\n            mock_decode.side_effect = Exception(\"Invalid base64\")\n\n            # Should handle the error gracefully - the exception should propagate\n            with pytest.raises(Exception, match=\"Invalid base64\"):\n                renderer.render(Sender.TOOL, tool_result)\n"
  },
  {
    "path": "browser-use-demo/tests/test_sampling_loop.py",
    "content": "\"\"\"\nComprehensive tests for the sampling loop and message handling.\n\"\"\"\n\nimport asyncio\nfrom unittest.mock import AsyncMock, Mock, patch\n\nimport pytest\nfrom browser_use_demo.loop import APIProvider, sampling_loop\nfrom browser_use_demo.message_handler import (\n    MessageBuilder,\n    ResponseProcessor,\n)\nfrom browser_use_demo.tools import ToolResult\n\n\nclass TestResponseProcessor:\n    \"\"\"Test the ResponseProcessor class.\"\"\"\n\n    def test_process_response_text_only(self):\n        \"\"\"Test processing a response with only text content.\"\"\"\n        mock_response = Mock()\n        mock_response.content = [\n            Mock(type=\"text\", text=\"This is a text response\")\n        ]\n\n        processor = ResponseProcessor()\n        result = processor.process_response(mock_response)\n\n        assert result.has_text is True\n        assert result.has_tools is False\n        assert len(result.assistant_content) == 1\n        assert result.assistant_content[0][\"type\"] == \"text\"\n        assert result.assistant_content[0][\"text\"] == \"This is a text response\"\n        assert len(result.tool_uses) == 0\n\n    def test_process_response_tool_only(self):\n        \"\"\"Test processing a response with only tool use.\"\"\"\n        mock_response = Mock()\n        mock_tool_use = Mock(\n            type=\"tool_use\",\n            id=\"tool_123\",\n            name=\"browser\",\n            input={\"action\": \"screenshot\"}\n        )\n        mock_response.content = [mock_tool_use]\n\n        processor = ResponseProcessor()\n        result = processor.process_response(mock_response)\n\n        assert result.has_text is False\n        assert result.has_tools is True\n        assert len(result.assistant_content) == 1\n        assert result.assistant_content[0][\"type\"] == \"tool_use\"\n        assert len(result.tool_uses) == 1\n\n    def test_process_response_mixed_content(self):\n        \"\"\"Test processing a response with both text and tool use.\"\"\"\n        mock_response = Mock()\n        mock_response.content = [\n            Mock(type=\"text\", text=\"Let me take a screenshot\"),\n            Mock(\n                type=\"tool_use\",\n                id=\"tool_456\",\n                name=\"browser\",\n                input={\"action\": \"screenshot\"}\n            )\n        ]\n\n        processor = ResponseProcessor()\n        result = processor.process_response(mock_response)\n\n        assert result.has_text is True\n        assert result.has_tools is True\n        assert len(result.assistant_content) == 2\n        assert result.assistant_content[0][\"type\"] == \"text\"\n        assert result.assistant_content[0][\"text\"] == \"Let me take a screenshot\"\n        assert result.assistant_content[1][\"type\"] == \"tool_use\"\n        assert len(result.tool_uses) == 1\n\n    def test_execute_tools_success(self):\n        \"\"\"Test successful tool execution.\"\"\"\n\n        async def run_test():\n            mock_tool = AsyncMock(return_value=ToolResult(output=\"Tool executed\"))\n            mock_collection = Mock()\n            mock_collection.tool_map = {\"browser\": mock_tool}\n\n            tool_uses = [\n                {\n                    \"type\": \"tool_use\",\n                    \"id\": \"tool_789\",\n                    \"name\": \"browser\",\n                    \"input\": {\"action\": \"screenshot\"}\n                }\n            ]\n\n            processor = ResponseProcessor()\n            results = await processor.execute_tools(tool_uses, mock_collection)\n\n            assert len(results) == 1\n            assert results[0][\"type\"] == \"tool_result\"\n            assert results[0][\"tool_use_id\"] == \"tool_789\"\n            assert any(\n                block.get(\"text\") == \"Tool executed\"\n                for block in results[0][\"content\"]\n            )\n\n        asyncio.run(run_test())\n\n    def test_execute_tools_with_error(self):\n        \"\"\"Test tool execution with error.\"\"\"\n\n        async def run_test():\n            mock_tool = AsyncMock(side_effect=Exception(\"Tool failed\"))\n            mock_collection = Mock()\n            mock_collection.tool_map = {\"browser\": mock_tool}\n\n            tool_uses = [\n                {\n                    \"type\": \"tool_use\",\n                    \"id\": \"tool_error\",\n                    \"name\": \"browser\",\n                    \"input\": {\"action\": \"invalid\"}\n                }\n            ]\n\n            processor = ResponseProcessor()\n            results = await processor.execute_tools(tool_uses, mock_collection)\n\n            assert len(results) == 1\n            assert results[0][\"type\"] == \"tool_result\"\n            assert results[0][\"is_error\"] is True\n            assert any(\n                \"Tool failed\" in block.get(\"text\", \"\")\n                for block in results[0][\"content\"]\n            )\n\n        asyncio.run(run_test())\n\n    def test_build_tool_result_with_image(self):\n        \"\"\"Test building tool result with base64 image.\"\"\"\n        result = ToolResult(base64_image=\"base64_data_here\")\n        processor = ResponseProcessor()\n\n        tool_result = processor._build_tool_result(result, \"tool_img\")\n\n        assert tool_result[\"type\"] == \"tool_result\"\n        assert tool_result[\"tool_use_id\"] == \"tool_img\"\n        assert any(\n            block.get(\"type\") == \"image\"\n            for block in tool_result[\"content\"]\n        )\n\n    def test_build_tool_result_with_text_extraction_markers(self):\n        \"\"\"Test handling of text extraction markers in tool results.\"\"\"\n        result = ToolResult(\n            output=\"__PAGE_EXTRACTED__\\nSome content\\n__FULL_CONTENT__\\nThe actual content\"\n        )\n        processor = ResponseProcessor()\n\n        tool_result = processor._build_tool_result(result, \"tool_extract\")\n\n        assert any(\n            block.get(\"text\") == \"The actual content\"\n            for block in tool_result[\"content\"]\n        )\n\n\nclass TestMessageBuilder:\n    \"\"\"Test the MessageBuilder class.\"\"\"\n\n    def test_add_assistant_message(self):\n        \"\"\"Test adding an assistant message.\"\"\"\n        messages = []\n        content = [\n            {\"type\": \"text\", \"text\": \"Hello\"},\n            {\"type\": \"tool_use\", \"id\": \"123\", \"name\": \"test\", \"input\": {}}\n        ]\n\n        builder = MessageBuilder()\n        builder.add_assistant_message(messages, content)\n\n        assert len(messages) == 1\n        assert messages[0][\"role\"] == \"assistant\"\n        assert messages[0][\"content\"] == content\n\n    def test_add_assistant_message_empty_content(self):\n        \"\"\"Test that empty content is not added.\"\"\"\n        messages = []\n        content = []\n\n        builder = MessageBuilder()\n        builder.add_assistant_message(messages, content)\n\n        assert len(messages) == 0\n\n    def test_add_tool_results(self):\n        \"\"\"Test adding tool results.\"\"\"\n        messages = []\n        tool_results = [\n            {\n                \"type\": \"tool_result\",\n                \"tool_use_id\": \"123\",\n                \"content\": [{\"type\": \"text\", \"text\": \"Result\"}]\n            }\n        ]\n\n        builder = MessageBuilder()\n        builder.add_tool_results(messages, tool_results)\n\n        assert len(messages) == 1\n        assert messages[0][\"role\"] == \"user\"\n        assert messages[0][\"content\"] == tool_results\n\n    def test_add_tool_results_empty(self):\n        \"\"\"Test that empty tool results are not added.\"\"\"\n        messages = []\n        tool_results = []\n\n        builder = MessageBuilder()\n        builder.add_tool_results(messages, tool_results)\n\n        assert len(messages) == 0\n\n    def test_ensure_message_integrity_valid(self):\n        \"\"\"Test message integrity validation with valid messages.\"\"\"\n        messages = [\n            {\"role\": \"user\", \"content\": \"Hello\"},\n            {\"role\": \"assistant\", \"content\": [{\"type\": \"text\", \"text\": \"Hi\"}]},\n        ]\n\n        builder = MessageBuilder()\n        assert builder.ensure_message_integrity(messages) is True\n\n    def test_ensure_message_integrity_missing_role(self):\n        \"\"\"Test message integrity with missing role.\"\"\"\n        messages = [\n            {\"content\": \"Hello\"},\n        ]\n\n        builder = MessageBuilder()\n        assert builder.ensure_message_integrity(messages) is False\n\n    def test_ensure_message_integrity_missing_content(self):\n        \"\"\"Test message integrity with missing content.\"\"\"\n        messages = [\n            {\"role\": \"user\"},\n        ]\n\n        builder = MessageBuilder()\n        assert builder.ensure_message_integrity(messages) is False\n\n    def test_ensure_message_integrity_empty_list_content(self):\n        \"\"\"Test message integrity with empty content list.\"\"\"\n        messages = [\n            {\"role\": \"user\", \"content\": []},\n        ]\n\n        builder = MessageBuilder()\n        assert builder.ensure_message_integrity(messages) is False\n\n    def test_extract_text_from_message(self):\n        \"\"\"Test extracting text from assistant message.\"\"\"\n        message = {\n            \"role\": \"assistant\",\n            \"content\": [\n                {\"type\": \"text\", \"text\": \"First part\"},\n                {\"type\": \"tool_use\", \"id\": \"123\", \"name\": \"test\", \"input\": {}},\n                {\"type\": \"text\", \"text\": \"Second part\"}\n            ]\n        }\n\n        builder = MessageBuilder()\n        text = builder.extract_text_from_message(message)\n\n        assert text == \"First part Second part\"\n\n    def test_extract_text_from_message_no_text(self):\n        \"\"\"Test extracting text when there's no text content.\"\"\"\n        message = {\n            \"role\": \"assistant\",\n            \"content\": [\n                {\"type\": \"tool_use\", \"id\": \"123\", \"name\": \"test\", \"input\": {}}\n            ]\n        }\n\n        builder = MessageBuilder()\n        text = builder.extract_text_from_message(message)\n\n        assert text is None\n\n    def test_extract_text_from_user_message(self):\n        \"\"\"Test that text extraction returns None for non-assistant messages.\"\"\"\n        message = {\n            \"role\": \"user\",\n            \"content\": \"User message\"\n        }\n\n        builder = MessageBuilder()\n        text = builder.extract_text_from_message(message)\n\n        assert text is None\n\n\n@pytest.mark.integration\nclass TestSamplingLoopIntegration:\n    \"\"\"Integration tests for the sampling loop.\"\"\"\n\n    @patch(\"browser_use_demo.loop.Anthropic\")\n    def test_sampling_loop_preserves_text_with_tools(self, mock_anthropic):\n        \"\"\"Test that text is preserved when tools are used.\"\"\"\n\n        async def run_test():\n            mock_client = Mock()\n            mock_anthropic.return_value = mock_client\n\n            mock_response = Mock()\n            mock_response.content = [\n                Mock(type=\"text\", text=\"I'll help you with that\"),\n                Mock(\n                    type=\"tool_use\",\n                    id=\"tool_001\",\n                    name=\"browser\",\n                    input={\"action\": \"screenshot\"}\n                )\n            ]\n\n            mock_client.beta.messages.create = Mock(return_value=mock_response)\n\n            mock_browser = AsyncMock()\n            mock_browser.return_value = ToolResult(output=\"Screenshot taken\")\n\n            messages = [{\"role\": \"user\", \"content\": \"Take a screenshot\"}]\n            output_messages = []\n            tool_outputs = {}\n\n            def output_callback(content):\n                output_messages.append(content)\n\n            def tool_output_callback(result, tool_id):\n                tool_outputs[tool_id] = result\n\n            updated_messages = await sampling_loop(\n                model=\"claude-sonnet-4-5\",\n                provider=APIProvider.ANTHROPIC,\n                system_prompt_suffix=\"\",\n                messages=messages,\n                output_callback=output_callback,\n                tool_output_callback=tool_output_callback,\n                api_response_callback=lambda *args: None,\n                api_key=\"test_key\",\n                browser_tool=mock_browser\n            )\n\n            api_call_args = mock_client.beta.messages.create.call_args[1]\n            assert api_call_args[\"tool_choice\"] == {\"type\": \"auto\"}\n\n            assert len(output_messages) >= 2\n            assert any(\n                msg.get(\"type\") == \"text\" and \"help you\" in msg.get(\"text\", \"\")\n                for msg in output_messages\n            )\n            assert any(msg.get(\"type\") == \"tool_use\" for msg in output_messages)\n\n            assistant_msgs = [m for m in updated_messages if m[\"role\"] == \"assistant\"]\n            assert len(assistant_msgs) > 0\n\n            last_assistant = assistant_msgs[-1]\n            assert isinstance(last_assistant[\"content\"], list)\n\n            has_text = any(\n                block.get(\"type\") == \"text\"\n                for block in last_assistant[\"content\"]\n            )\n            has_tool = any(\n                block.get(\"type\") == \"tool_use\"\n                for block in last_assistant[\"content\"]\n            )\n\n            assert has_text and has_tool, \"Assistant message should contain both text and tool use\"\n\n        asyncio.run(run_test())\n\n    @patch(\"browser_use_demo.loop.Anthropic\")\n    def test_sampling_loop_text_only_response(self, mock_anthropic):\n        \"\"\"Test handling of text-only responses.\"\"\"\n\n        async def run_test():\n            mock_client = Mock()\n            mock_anthropic.return_value = mock_client\n\n            mock_response = Mock()\n            mock_response.content = [\n                Mock(type=\"text\", text=\"This is just a text response\")\n            ]\n\n            mock_client.beta.messages.create = Mock(return_value=mock_response)\n\n            messages = [{\"role\": \"user\", \"content\": \"Hello\"}]\n\n            updated_messages = await sampling_loop(\n                model=\"claude-sonnet-4-5\",\n                provider=APIProvider.ANTHROPIC,\n                system_prompt_suffix=\"\",\n                messages=messages,\n                output_callback=lambda x: None,\n                tool_output_callback=lambda r, i: None,\n                api_response_callback=lambda *args: None,\n                api_key=\"test_key\"\n            )\n\n            assert len(updated_messages) == 2\n            assert updated_messages[-1][\"role\"] == \"assistant\"\n            assert any(\n                block.get(\"text\") == \"This is just a text response\"\n                for block in updated_messages[-1][\"content\"]\n            )\n\n        asyncio.run(run_test())\n\n    @patch(\"browser_use_demo.loop.Anthropic\")\n    def test_sampling_loop_multiple_tools_with_text(self, mock_anthropic):\n        \"\"\"Test handling of multiple tool uses with text.\"\"\"\n\n        async def run_test():\n            mock_client = Mock()\n            mock_anthropic.return_value = mock_client\n\n            mock_response = Mock()\n            mock_response.content = [\n                Mock(type=\"text\", text=\"I'll perform multiple actions\"),\n                Mock(\n                    type=\"tool_use\",\n                    id=\"tool_001\",\n                    name=\"browser\",\n                    input={\"action\": \"screenshot\"}\n                ),\n                Mock(type=\"text\", text=\"Now navigating\"),\n                Mock(\n                    type=\"tool_use\",\n                    id=\"tool_002\",\n                    name=\"browser\",\n                    input={\"action\": \"navigate\", \"url\": \"example.com\"}\n                )\n            ]\n\n            mock_client.beta.messages.create = Mock(return_value=mock_response)\n\n            mock_browser = AsyncMock()\n            mock_browser.return_value = ToolResult(output=\"Action completed\")\n\n            messages = [{\"role\": \"user\", \"content\": \"Do multiple things\"}]\n\n            updated_messages = await sampling_loop(\n                model=\"claude-sonnet-4-5\",\n                provider=APIProvider.ANTHROPIC,\n                system_prompt_suffix=\"\",\n                messages=messages,\n                output_callback=lambda x: None,\n                tool_output_callback=lambda r, i: None,\n                api_response_callback=lambda *args: None,\n                api_key=\"test_key\",\n                browser_tool=mock_browser\n            )\n\n            assistant_msgs = [m for m in updated_messages if m[\"role\"] == \"assistant\"]\n            last_assistant = assistant_msgs[-1]\n\n            text_blocks = [\n                block for block in last_assistant[\"content\"]\n                if block.get(\"type\") == \"text\"\n            ]\n            tool_blocks = [\n                block for block in last_assistant[\"content\"]\n                if block.get(\"type\") == \"tool_use\"\n            ]\n\n            assert len(text_blocks) == 2\n            assert len(tool_blocks) == 2\n\n        asyncio.run(run_test())\n\n    @patch(\"browser_use_demo.loop.Anthropic\")\n    def test_tool_choice_parameter_set(self, mock_anthropic):\n        \"\"\"Test that tool_choice is explicitly set to auto.\"\"\"\n\n        async def run_test():\n            mock_client = Mock()\n            mock_anthropic.return_value = mock_client\n\n            mock_response = Mock()\n            mock_response.content = [Mock(type=\"text\", text=\"Response\")]\n\n            mock_client.beta.messages.create = Mock(return_value=mock_response)\n\n            await sampling_loop(\n                model=\"claude-sonnet-4-5\",\n                provider=APIProvider.ANTHROPIC,\n                system_prompt_suffix=\"\",\n                messages=[{\"role\": \"user\", \"content\": \"Test\"}],\n                output_callback=lambda x: None,\n                tool_output_callback=lambda r, i: None,\n                api_response_callback=lambda *args: None,\n                api_key=\"test_key\"\n            )\n\n            call_args = mock_client.beta.messages.create.call_args[1]\n            assert \"tool_choice\" in call_args\n            assert call_args[\"tool_choice\"] == {\"type\": \"auto\"}\n\n        asyncio.run(run_test())\n"
  },
  {
    "path": "browser-use-demo/tests/test_streamlit_helpers.py",
    "content": "\"\"\"Tests for Streamlit helper functions with edge case coverage.\"\"\"\n\nimport asyncio\nfrom unittest.mock import MagicMock, Mock, patch\n\nimport pytest\nfrom browser_use_demo.loop import APIProvider\nfrom browser_use_demo.streamlit import (\n    authenticate,\n    get_or_create_event_loop,\n    setup_state,\n)\n\n\nclass TestSetupState:\n    \"\"\"Test suite for setup_state function.\"\"\"\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    def test_setup_state_fresh_initialization(self, mock_state, mock_environment):\n        \"\"\"Test setup_state with completely empty session state.\"\"\"\n        # Simulate empty session state\n        mock_state.__contains__.return_value = False\n        # Set provider to valid enum value so lambda can access it\n        mock_state.provider = APIProvider.ANTHROPIC\n\n        with patch(\"browser_use_demo.tools.BrowserTool\") as mock_browser:\n            setup_state()\n\n            # Check all defaults were set\n            assert \"messages\" in mock_state.__setitem__.call_args_list[0][0]\n            assert \"api_key\" in str(mock_state.__setitem__.call_args_list)\n            assert \"event_loop\" in str(mock_state.__setitem__.call_args_list)\n\n            # Browser tool should be created\n            mock_browser.assert_called_once()\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    def test_setup_state_partial_initialization(self, mock_state):\n        \"\"\"Test setup_state when some keys already exist.\"\"\"\n\n        # Simulate partial state\n        existing_keys = [\"messages\", \"api_key\"]\n\n        def contains_side_effect(key):\n            return key in existing_keys\n\n        mock_state.__contains__.side_effect = contains_side_effect\n        # Set provider to valid enum value so lambda can access it\n        mock_state.provider = APIProvider.ANTHROPIC\n\n        with patch(\"browser_use_demo.tools.BrowserTool\"):\n            setup_state()\n\n            # Only missing keys should be set\n            set_keys = [call[0][0] for call in mock_state.__setitem__.call_args_list]\n            assert \"messages\" not in set_keys\n            assert \"api_key\" not in set_keys\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    def test_setup_state_missing_env_variables(self, mock_state, clean_environment):\n        \"\"\"Test setup_state when environment variables are missing.\"\"\"\n\n        mock_state.__contains__.return_value = False\n        # Set provider to valid enum value so lambda can access it\n        mock_state.provider = APIProvider.ANTHROPIC\n\n        with patch(\"browser_use_demo.tools.BrowserTool\") as mock_browser:\n            setup_state()\n\n            # BrowserTool no longer takes dimensions as arguments\n            mock_browser.assert_called_with()\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    def test_setup_state_lambda_evaluation(self, mock_state, mock_provider):\n        \"\"\"Test that lambda functions are evaluated correctly.\"\"\"\n\n        mock_state.__contains__.return_value = False\n        mock_state.provider = mock_provider.ANTHROPIC\n\n        setup_state()\n\n        # Model should be set based on provider\n        model_calls = [\n            call\n            for call in mock_state.__setitem__.call_args_list\n            if call[0][0] == \"model\"\n        ]\n        assert len(model_calls) > 0\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    def test_setup_state_browser_tool_error(self, mock_state):\n        \"\"\"Test setup_state when BrowserTool initialization fails.\"\"\"\n\n        mock_state.__contains__.return_value = False\n        # Set provider to valid enum value so lambda can access it\n        mock_state.provider = APIProvider.ANTHROPIC\n\n        with patch(\"browser_use_demo.tools.BrowserTool\") as mock_browser:\n            mock_browser.side_effect = Exception(\"Browser init failed\")\n\n            # Should raise the exception\n            with pytest.raises(Exception, match=\"Browser init failed\"):\n                setup_state()\n\n    # Test removed - BrowserTool no longer reads dimensions from environment\n\n\nclass TestGetOrCreateEventLoop:\n    \"\"\"Test suite for get_or_create_event_loop function.\"\"\"\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    @patch(\"asyncio.new_event_loop\")\n    @patch(\"asyncio.set_event_loop\")\n    def test_create_new_loop_when_none(self, mock_set_loop, mock_new_loop, mock_state):\n        \"\"\"Test creating new event loop when none exists.\"\"\"\n\n        mock_state.event_loop = None\n        new_loop = Mock()\n        mock_new_loop.return_value = new_loop\n\n        result = get_or_create_event_loop()\n\n        mock_new_loop.assert_called_once()\n        mock_set_loop.assert_called_once_with(new_loop)\n        assert mock_state.event_loop == new_loop\n        assert result == new_loop\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    @patch(\"asyncio.new_event_loop\")\n    @patch(\"asyncio.set_event_loop\")\n    def test_create_new_loop_when_closed(\n        self, mock_set_loop, mock_new_loop, mock_state\n    ):\n        \"\"\"Test creating new event loop when existing is closed.\"\"\"\n\n        closed_loop = Mock()\n        closed_loop.is_closed.return_value = True\n        mock_state.event_loop = closed_loop\n\n        new_loop = Mock()\n        mock_new_loop.return_value = new_loop\n\n        result = get_or_create_event_loop()\n\n        mock_new_loop.assert_called_once()\n        mock_set_loop.assert_called_once_with(new_loop)\n        assert mock_state.event_loop == new_loop\n        assert result == new_loop\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    @patch(\"asyncio.new_event_loop\")\n    @patch(\"asyncio.set_event_loop\")\n    def test_reuse_existing_open_loop(self, mock_set_loop, mock_new_loop, mock_state):\n        \"\"\"Test reusing existing open event loop.\"\"\"\n\n        existing_loop = Mock()\n        existing_loop.is_closed.return_value = False\n        mock_state.event_loop = existing_loop\n\n        result = get_or_create_event_loop()\n\n        mock_new_loop.assert_not_called()\n        mock_set_loop.assert_called_once_with(existing_loop)\n        assert result == existing_loop\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    @patch(\"asyncio.new_event_loop\")\n    def test_event_loop_creation_error(self, mock_new_loop, mock_state):\n        \"\"\"Test handling error during event loop creation.\"\"\"\n\n        mock_state.event_loop = None\n        mock_new_loop.side_effect = RuntimeError(\"Cannot create loop\")\n\n        with pytest.raises(RuntimeError, match=\"Cannot create loop\"):\n            get_or_create_event_loop()\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    @patch(\"asyncio.set_event_loop\")\n    def test_set_event_loop_error(self, mock_set_loop, mock_state):\n        \"\"\"Test handling error when setting event loop.\"\"\"\n\n        mock_state.event_loop = None\n        mock_set_loop.side_effect = RuntimeError(\"Cannot set loop\")\n\n        with pytest.raises(RuntimeError, match=\"Cannot set loop\"):\n            get_or_create_event_loop()\n\n\nclass TestAuthenticate:\n    \"\"\"Test suite for authenticate function.\"\"\"\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    @patch(\"streamlit.error\")\n    @patch(\"streamlit.stop\")\n    def test_authenticate_with_valid_key(\n        self, mock_stop, mock_error, mock_state, mock_provider\n    ):\n        \"\"\"Test authenticate with valid API key.\"\"\"\n\n        mock_state.provider = mock_provider.ANTHROPIC\n        mock_state.api_key = \"valid-key\"\n\n        result = authenticate()\n\n        assert result is True\n        mock_error.assert_not_called()\n        mock_stop.assert_not_called()\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    @patch(\"streamlit.error\")\n    @patch(\"streamlit.stop\")\n    def test_authenticate_with_missing_key(\n        self, mock_stop, mock_error, mock_state, mock_provider\n    ):\n        \"\"\"Test authenticate with missing API key.\"\"\"\n\n        mock_state.provider = mock_provider.ANTHROPIC\n        mock_state.api_key = \"\"\n\n        authenticate()\n\n        mock_error.assert_called_once_with(\n            \"Please provide your Anthropic API key in the sidebar\"\n        )\n        mock_stop.assert_called_once()\n        # Function doesn't return after stop() in real scenario\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    @patch(\"streamlit.error\")\n    @patch(\"streamlit.stop\")\n    def test_authenticate_with_none_key(\n        self, mock_stop, mock_error, mock_state, mock_provider\n    ):\n        \"\"\"Test authenticate with None API key.\"\"\"\n\n        mock_state.provider = mock_provider.ANTHROPIC\n        mock_state.api_key = None\n\n        authenticate()\n\n        mock_error.assert_called_once()\n        mock_stop.assert_called_once()\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    def test_authenticate_non_anthropic_provider(self, mock_state, mock_provider):\n        \"\"\"Test authenticate with non-Anthropic provider.\"\"\"\n\n        mock_state.provider = mock_provider.BEDROCK\n        mock_state.api_key = \"\"  # Empty key should be OK for non-Anthropic\n\n        result = authenticate()\n\n        assert result is True\n\n\nclass TestEdgeCasesAndErrors:\n    \"\"\"Test edge cases and error conditions for helper functions.\"\"\"\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    def test_setup_state_with_corrupted_state(self, mock_state):\n        \"\"\"Test setup_state with corrupted session state.\"\"\"\n\n        # Simulate corrupted state that raises on access\n        mock_state.__contains__.side_effect = Exception(\"State corrupted\")\n\n        with pytest.raises(Exception, match=\"State corrupted\"):\n            setup_state()\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    def test_concurrent_setup_state_calls(self, mock_state):\n        \"\"\"Test concurrent calls to setup_state.\"\"\"\n        import threading\n\n        mock_state.__contains__.return_value = False\n        # Set provider to valid enum value so lambda can access it\n        mock_state.provider = APIProvider.ANTHROPIC\n        errors = []\n\n        def run_setup():\n            try:\n                with patch(\"browser_use_demo.tools.BrowserTool\"):\n                    setup_state()\n            except Exception as e:\n                errors.append(e)\n\n        threads = [threading.Thread(target=run_setup) for _ in range(5)]\n        for t in threads:\n            t.start()\n        for t in threads:\n            t.join()\n\n        # Should handle concurrent access without crashes\n        assert len(errors) == 0\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    @patch(\"asyncio.get_event_loop\")\n    def test_get_or_create_with_running_loop(self, mock_get_loop, mock_state):\n        \"\"\"Test get_or_create_event_loop when another loop is running.\"\"\"\n\n        mock_state.event_loop = None\n        running_loop = Mock(spec=asyncio.AbstractEventLoop)\n        running_loop.is_running.return_value = True\n        mock_get_loop.return_value = running_loop\n\n        # Should create new loop despite running loop exists\n        with patch(\"asyncio.new_event_loop\") as mock_new:\n            new_loop = Mock(spec=asyncio.AbstractEventLoop)\n            new_loop.is_closed.return_value = False\n            mock_new.return_value = new_loop\n\n            with patch(\"asyncio.set_event_loop\"):\n                result = get_or_create_event_loop()\n                assert result == new_loop\n\n    @patch(\"streamlit.session_state\", new_callable=MagicMock)\n    def test_setup_state_with_readonly_state(self, mock_state):\n        \"\"\"Test setup_state when session state is read-only.\"\"\"\n\n        mock_state.__contains__.return_value = False\n        mock_state.__setitem__.side_effect = AttributeError(\"Read-only state\")\n\n        with pytest.raises(AttributeError, match=\"Read-only state\"):\n            setup_state()\n\n    # Test removed - BrowserTool no longer reads dimensions from environment\n"
  },
  {
    "path": "browser-use-demo/validate_env.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Validate environment configuration before startup.\"\"\"\n\nimport os\nimport sys\nfrom pathlib import Path\n\n# Import constants for display information\ntry:\n    from browser_use_demo.display_constants import BROWSER_WIDTH, BROWSER_HEIGHT, DISPLAY_WIDTH, DISPLAY_HEIGHT\nexcept ImportError:\n    # Fallback if running outside the package\n    DISPLAY_WIDTH = 1920\n    DISPLAY_HEIGHT = 1080\n    BROWSER_WIDTH = 1920\n    BROWSER_HEIGHT = 1080\n\n\ndef validate_env():\n    \"\"\"Validate required environment variables are set.\"\"\"\n    # Check API key\n    api_key = os.environ.get(\"ANTHROPIC_API_KEY\")\n\n    if not api_key:\n        print(\"\\n\" + \"=\" * 60)\n        print(\"ERROR: Missing required configuration!\")\n        print(\"=\" * 60)\n        print(\"\\nThe Browser Use Demo requires proper configuration to run.\")\n        print(\"\\n🔧 RECOMMENDED: Use docker-compose with a .env file:\")\n        print(\"  1. Copy the example environment file:\")\n        print(\"     cp .env.example .env\")\n        print(\"  2. Edit .env and add your Anthropic API key\")\n        print(\"  3. Run with docker-compose:\")\n        print(\"     docker-compose up --build\")\n        print(\"=\" * 60)\n        sys.exit(1)\n\n    if api_key == \"your_anthropic_api_key_here\" or len(api_key) < 10:\n        print(\"\\n\" + \"=\" * 60)\n        print(\"ERROR: Invalid API key!\")\n        print(\"=\" * 60)\n        print(\"  ANTHROPIC_API_KEY: Must be a valid API key\")\n        print(\"\\nTo fix this, please edit your .env file with a valid API key\")\n        print(\"=\" * 60)\n        sys.exit(1)\n\n    print(\"\\n✓ Environment validation passed\")\n    print(f\"  Display: {DISPLAY_WIDTH}x{DISPLAY_HEIGHT}\")\n    print(f\"  Browser: {BROWSER_WIDTH}x{BROWSER_HEIGHT}\")\n\n\nif __name__ == \"__main__\":\n    validate_env()\n"
  },
  {
    "path": "computer-use-demo/.gitignore",
    "content": ".venv\n.ruff_cache\n__pycache__\n.pytest_cache\n"
  },
  {
    "path": "computer-use-demo/.zed/settings.json",
    "content": "{\n  \"preferred_line_length\": 88,\n  \"languages\": {\n    \"Python\": {\n      \"language_servers\": [\"pyright\", \"ruff\"]\n    }\n  },\n  \"telemetry\": {\n    \"diagnostics\": false,\n    \"metrics\": false\n  }\n}\n"
  },
  {
    "path": "computer-use-demo/CONTRIBUTING.md",
    "content": "# Contributing Guidelines\n\nThank you for your interest in contributing to the Computer Use Demo quickstart! This document outlines the process and guidelines for contributing to this project.\n\n## Code of Conduct\n\nPlease be respectful and constructive in all interactions. We aim to maintain a welcoming and inclusive environment for all contributors.\n\n## Contribution Policy\n\n- Bugfixes and updates to our documentation that address correctness issues are always welcome\n- Feature additions, refactors, and documentation updates beyond the scope of correctness (major changes) are accepted at the sole determination of the maintainers. We require each major change to be submitted in a separate Pull Request. We will assess new features under the following criteria:\n  - Adherence to coding standards\n  - Ease of use as a reference implementation\n  - User experience\n  - Applicability to a wide audience of developers\n  - Minimization of third-party dependencies\n  - Does not promote a product or service\n\nPlease open a github issue if you are need clarification on this policy or you want to discuss a new feature addition.\n\n## Development Setup\n\n1. Create and activate a Python virtual environment:\n\n   ```bash\n   python -m venv .venv\n   source .venv/bin/activate  # On Unix\n   # or\n   .venv\\Scripts\\activate  # On Windows\n   ```\n\n2. Install development dependencies:\n\n   ```bash\n   pip install -r dev-requirements.txt\n   ```\n\n3. Install pre-commit hooks:\n   ```bash\n   pre-commit install\n   ```\n\n## Development Process\n\n1. Fork the repository and create a branch for your changes\n2. Make your changes following our coding standards\n3. Submit a pull request with a clear description of the changes\n\n## Coding Standards\n\n- Use clear, descriptive variable and function names\n- Follow PEP 8 style guidelines for Python code\n- Keep functions focused and single-purpose\n- Avoid inline comments - code should be self-documenting\n- Use type hints for all Python functions\n- Use dataclasses for structured data (see `tools/base.py` for examples)\n- All tools must inherit from `BaseAnthropicTool` and implement required methods\n- Use abstract base classes (ABC) for defining interfaces\n- Handle errors using `ToolError` and `ToolFailure` classes\n\n## Code Quality Tools\n\nWe use several tools to maintain code quality:\n\n- **Ruff**: For linting and formatting\n  - Run `ruff check .` for linting\n  - Run `ruff format .` for formatting\n  - See `ruff.toml` for enabled rules\n- **Pyright**: For type checking\n  - Configuration in `pyproject.toml`\n- **Pre-commit**: For automated checks before commits\n\n## Testing\n\n- Add tests for new functionality in the `tests/` directory\n- Follow existing test patterns (see `tests/tools/` for examples)\n- Use pytest fixtures where appropriate\n- Run tests with:\n  ```bash\n  pytest\n  ```\n- Tests must pass in async mode (configured in pyproject.toml)\n\n## Commit Guidelines\n\n- All commits MUST be signed (use `git commit -S`)\n- Write clear, descriptive commit messages\n- Use present tense (\"Add feature\" not \"Added feature\")\n- Reference issue numbers when applicable\n\n## Pull Request Process\n\n1. Update documentation as needed\n2. Add tests for new functionality\n3. Ensure all checks pass:\n   - All tests pass\n   - Ruff linting passes\n   - Type checking passes\n   - Pre-commit hooks pass\n4. Request review from maintainers\n5. Address review feedback\n\n## Tool Development\n\nWhen creating new tools:\n\n1. Inherit from `BaseAnthropicTool`\n2. Implement `__call__` and `to_params` methods\n3. Use appropriate result types (`ToolResult`, `CLIResult`, or `ToolFailure`)\n4. Add comprehensive tests\n5. Document parameters and return types\n\n## Documentation\n\n- Keep README.md up to date\n- Document new features and changes\n- Use clear, concise language\n- Include docstrings for all public classes and methods\n- Use concise, single-line docstrings for simple functions\n- For complex functions, include:\n  - A brief description\n  - Args/parameters if not obvious\n  - Return value if not obvious\n  - Any important notes about behavior\n\n## Questions?\n\nIf you have questions, please open an issue for discussion.\n"
  },
  {
    "path": "computer-use-demo/Dockerfile",
    "content": "FROM docker.io/ubuntu:22.04\n\nENV DEBIAN_FRONTEND=noninteractive\nENV DEBIAN_PRIORITY=high\n\nRUN apt-get update && \\\n    apt-get -y upgrade && \\\n    apt-get -y install \\\n    # UI Requirements\n    xvfb \\\n    xterm \\\n    xdotool \\\n    scrot \\\n    imagemagick \\\n    sudo \\\n    mutter \\\n    x11vnc \\\n    # Python/pyenv reqs\n    build-essential \\\n    libssl-dev  \\\n    zlib1g-dev \\\n    libbz2-dev \\\n    libreadline-dev \\\n    libsqlite3-dev \\\n    curl \\\n    git \\\n    libncursesw5-dev \\\n    xz-utils \\\n    tk-dev \\\n    libxml2-dev \\\n    libxmlsec1-dev \\\n    libffi-dev \\\n    liblzma-dev \\\n    # Network tools\n    net-tools \\\n    netcat \\\n    # PPA req\n    software-properties-common && \\\n    # Userland apps\n    sudo add-apt-repository ppa:mozillateam/ppa && \\\n    sudo apt-get install -y --no-install-recommends \\\n    libreoffice \\\n    firefox-esr \\\n    x11-apps \\\n    xpdf \\\n    gedit \\\n    xpaint \\\n    tint2 \\\n    galculator \\\n    pcmanfm \\\n    unzip && \\\n    apt-get clean\n\n# Install noVNC\nRUN git clone --branch v1.5.0 https://github.com/novnc/noVNC.git /opt/noVNC && \\\n    git clone --branch v0.12.0 https://github.com/novnc/websockify /opt/noVNC/utils/websockify && \\\n    ln -s /opt/noVNC/vnc.html /opt/noVNC/index.html\n\n# setup user\nENV USERNAME=computeruse\nENV HOME=/home/$USERNAME\nRUN useradd -m -s /bin/bash -d $HOME $USERNAME\nRUN echo \"${USERNAME} ALL=(ALL) NOPASSWD: ALL\" >> /etc/sudoers\nUSER computeruse\nWORKDIR $HOME\n\n# setup python\nRUN git clone https://github.com/pyenv/pyenv.git ~/.pyenv && \\\n    cd ~/.pyenv && src/configure && make -C src && cd .. && \\\n    echo 'export PYENV_ROOT=\"$HOME/.pyenv\"' >> ~/.bashrc && \\\n    echo 'command -v pyenv >/dev/null || export PATH=\"$PYENV_ROOT/bin:$PATH\"' >> ~/.bashrc && \\\n    echo 'eval \"$(pyenv init -)\"' >> ~/.bashrc\nENV PYENV_ROOT=\"$HOME/.pyenv\"\nENV PATH=\"$PYENV_ROOT/bin:$PATH\"\nENV PYENV_VERSION_MAJOR=3\nENV PYENV_VERSION_MINOR=11\nENV PYENV_VERSION_PATCH=6\nENV PYENV_VERSION=$PYENV_VERSION_MAJOR.$PYENV_VERSION_MINOR.$PYENV_VERSION_PATCH\nRUN eval \"$(pyenv init -)\" && \\\n    pyenv install $PYENV_VERSION && \\\n    pyenv global $PYENV_VERSION && \\\n    pyenv rehash\n\nENV PATH=\"$HOME/.pyenv/shims:$HOME/.pyenv/bin:$PATH\"\n\nRUN python -m pip install --upgrade pip==23.1.2 setuptools==58.0.4 wheel==0.40.0 && \\\n    python -m pip config set global.disable-pip-version-check true\n\n# only reinstall if requirements.txt changes\nCOPY --chown=$USERNAME:$USERNAME computer_use_demo/requirements.txt $HOME/computer_use_demo/requirements.txt\nRUN python -m pip install -r $HOME/computer_use_demo/requirements.txt\n\n# setup desktop env & app\nCOPY --chown=$USERNAME:$USERNAME image/ $HOME\nCOPY --chown=$USERNAME:$USERNAME computer_use_demo/ $HOME/computer_use_demo/\n\nARG DISPLAY_NUM=1\nARG HEIGHT=768\nARG WIDTH=1024\nENV DISPLAY_NUM=$DISPLAY_NUM\nENV HEIGHT=$HEIGHT\nENV WIDTH=$WIDTH\n\nENTRYPOINT [ \"./entrypoint.sh\" ]\n"
  },
  {
    "path": "computer-use-demo/LICENSE",
    "content": "Copyright 2024 Anthropic, PBC.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "computer-use-demo/README.md",
    "content": "# Anthropic Computer Use Demo\n\n> [!NOTE]\n> Now featuring support for the new Claude 4 models! The latest Claude Opus 4.5 (claude-opus-4-5-20251101) is now available, along with Claude Sonnet 4.5 (claude-sonnet-4-5-20250929), Claude Sonnet 4 (claude-sonnet-4-20250514), Claude Opus 4 (claude-opus-4-20250514), and Claude Haiku 4.5 (claude-haiku-4-5-20251001). These models bring next-generation capabilities with the updated str_replace_based_edit_tool that replaces the previous str_replace_editor tool. The undo_edit command has been removed in this latest version for a more streamlined experience.\n\n> [!CAUTION]\n> Computer use is a beta feature. Please be aware that computer use poses unique risks that are distinct from standard API features or chat interfaces. These risks are heightened when using computer use to interact with the internet. To minimize risks, consider taking precautions such as:\n>\n> 1. Use a dedicated virtual machine or container with minimal privileges to prevent direct system attacks or accidents.\n> 2. Avoid giving the model access to sensitive data, such as account login information, to prevent information theft.\n> 3. Limit internet access to an allowlist of domains to reduce exposure to malicious content.\n> 4. Ask a human to confirm decisions that may result in meaningful real-world consequences as well as any tasks requiring affirmative consent, such as accepting cookies, executing financial transactions, or agreeing to terms of service.\n>\n> In some circumstances, Claude will follow commands found in content even if it conflicts with the user's instructions. For example, instructions on webpages or contained in images may override user instructions or cause Claude to make mistakes. We suggest taking precautions to isolate Claude from sensitive data and actions to avoid risks related to prompt injection.\n>\n> Finally, please inform end users of relevant risks and obtain their consent prior to enabling computer use in your own products.\n\nThis repository helps you get started with computer use on Claude, with reference implementations of:\n\n- Build files to create a Docker container with all necessary dependencies\n- A computer use agent loop using the Claude API, Bedrock, or Vertex to access Claude Opus 4.5, Claude Sonnet 4.5, Claude Sonnet 4, Claude Opus 4, Claude Haiku 4.5, Claude 3.7 Sonnet, and Claude 3.5 Sonnet models\n- Anthropic-defined computer use tools\n- A streamlit app for interacting with the agent loop\n\nPlease use [this form](https://forms.gle/BT1hpBrqDPDUrCqo7) to provide feedback on the quality of the model responses, the API itself, or the quality of the documentation - we cannot wait to hear from you!\n\n> [!IMPORTANT]\n> The Beta API used in this reference implementation is subject to change. Please refer to the [API release notes](https://docs.claude.com/en/release-notes/api) for the most up-to-date information.\n\n> [!IMPORTANT]\n> The components are weakly separated: the agent loop runs in the container being controlled by Claude, can only be used by one session at a time, and must be restarted or reset between sessions if necessary.\n\n## Quickstart: running the Docker container\n\n### Claude API\n\n> [!TIP]\n> You can find your API key in the [Claude Console](https://console.anthropic.com/).\n\n```bash\nexport ANTHROPIC_API_KEY=%your_api_key%\ndocker run \\\n    -e ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \\\n    -v $HOME/.anthropic:/home/computeruse/.anthropic \\\n    -p 5900:5900 \\\n    -p 8501:8501 \\\n    -p 6080:6080 \\\n    -p 8080:8080 \\\n    -it ghcr.io/anthropics/anthropic-quickstarts:computer-use-demo-latest\n```\n\nOnce the container is running, see the [Accessing the demo app](#accessing-the-demo-app) section below for instructions on how to connect to the interface.\n\n### Bedrock\n\n> [!TIP]\n> To use the new Claude 3.7 Sonnet on Bedrock, you first need to [request model access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access-modify.html).\n\nYou'll need to pass in AWS credentials with appropriate permissions to use Claude on Bedrock.\nYou have a few options for authenticating with Bedrock. See the [boto3 documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html#environment-variables) for more details and options.\n\n#### Option 1: (suggested) Use the host's AWS credentials file and AWS profile\n\n```bash\nexport AWS_PROFILE=<your_aws_profile>\ndocker run \\\n    -e API_PROVIDER=bedrock \\\n    -e AWS_PROFILE=$AWS_PROFILE \\\n    -e AWS_REGION=us-west-2 \\\n    -v $HOME/.aws:/home/computeruse/.aws \\\n    -v $HOME/.anthropic:/home/computeruse/.anthropic \\\n    -p 5900:5900 \\\n    -p 8501:8501 \\\n    -p 6080:6080 \\\n    -p 8080:8080 \\\n    -it ghcr.io/anthropics/anthropic-quickstarts:computer-use-demo-latest\n```\n\nOnce the container is running, see the [Accessing the demo app](#accessing-the-demo-app) section below for instructions on how to connect to the interface.\n\n#### Option 2: Use an access key and secret\n\n```bash\nexport AWS_ACCESS_KEY_ID=%your_aws_access_key%\nexport AWS_SECRET_ACCESS_KEY=%your_aws_secret_access_key%\nexport AWS_SESSION_TOKEN=%your_aws_session_token%\ndocker run \\\n    -e API_PROVIDER=bedrock \\\n    -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \\\n    -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \\\n    -e AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN \\\n    -e AWS_REGION=us-west-2 \\\n    -v $HOME/.anthropic:/home/computeruse/.anthropic \\\n    -p 5900:5900 \\\n    -p 8501:8501 \\\n    -p 6080:6080 \\\n    -p 8080:8080 \\\n    -it ghcr.io/anthropics/anthropic-quickstarts:computer-use-demo-latest\n```\n\nOnce the container is running, see the [Accessing the demo app](#accessing-the-demo-app) section below for instructions on how to connect to the interface.\n\n### Vertex\n\nYou'll need to pass in Google Cloud credentials with appropriate permissions to use Claude on Vertex.\n\n```bash\ndocker build . -t computer-use-demo\ngcloud auth application-default login\nexport VERTEX_REGION=%your_vertex_region%\nexport VERTEX_PROJECT_ID=%your_vertex_project_id%\ndocker run \\\n    -e API_PROVIDER=vertex \\\n    -e CLOUD_ML_REGION=$VERTEX_REGION \\\n    -e ANTHROPIC_VERTEX_PROJECT_ID=$VERTEX_PROJECT_ID \\\n    -v $HOME/.config/gcloud/application_default_credentials.json:/home/computeruse/.config/gcloud/application_default_credentials.json \\\n    -p 5900:5900 \\\n    -p 8501:8501 \\\n    -p 6080:6080 \\\n    -p 8080:8080 \\\n    -it computer-use-demo\n```\n\nOnce the container is running, see the [Accessing the demo app](#accessing-the-demo-app) section below for instructions on how to connect to the interface.\n\nThis example shows how to use the Google Cloud Application Default Credentials to authenticate with Vertex.\nYou can also set `GOOGLE_APPLICATION_CREDENTIALS` to use an arbitrary credential file, see the [Google Cloud Authentication documentation](https://cloud.google.com/docs/authentication/application-default-credentials#GAC) for more details.\n\n### Accessing the demo app\n\nOnce the container is running, open your browser to [http://localhost:8080](http://localhost:8080) to access the combined interface that includes both the agent chat and desktop view.\n\nThe container stores settings like the API key and custom system prompt in `~/.anthropic/`. Mount this directory to persist these settings between container runs.\n\nAlternative access points:\n\n- Streamlit interface only: [http://localhost:8501](http://localhost:8501)\n- Desktop view only: [http://localhost:6080/vnc.html](http://localhost:6080/vnc.html)\n- Direct VNC connection: `vnc://localhost:5900` (for VNC clients)\n\n## Screen size\n\nEnvironment variables `WIDTH` and `HEIGHT` can be used to set the screen size. For example:\n\n```bash\ndocker run \\\n    -e ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \\\n    -v $HOME/.anthropic:/home/computeruse/.anthropic \\\n    -p 5900:5900 \\\n    -p 8501:8501 \\\n    -p 6080:6080 \\\n    -p 8080:8080 \\\n    -e WIDTH=1920 \\\n    -e HEIGHT=1080 \\\n    -it ghcr.io/anthropics/anthropic-quickstarts:computer-use-demo-latest\n```\n\nWe do not recommend sending screenshots in resolutions above [XGA/WXGA](https://en.wikipedia.org/wiki/Display_resolution_standards#XGA) to avoid issues related to [image resizing](https://docs.claude.com/en/docs/build-with-claude/vision#evaluate-image-size).\nRelying on the image resizing behavior in the API will result in lower model accuracy and slower performance than implementing scaling in your tools directly. The `computer` tool implementation in this project demonstrates how to scale both images and coordinates from higher resolutions to the suggested resolutions.\n\nWhen implementing computer use yourself, we recommend using XGA resolution (1024x768):\n\n- For higher resolutions: Scale the image down to XGA and let the model interact with this scaled version, then map the coordinates back to the original resolution proportionally.\n- For lower resolutions or smaller devices (e.g. mobile devices): Add black padding around the display area until it reaches 1024x768.\n\n## Development\n\n```bash\n./setup.sh  # configure venv, install development dependencies, and install pre-commit hooks\ndocker build . -t computer-use-demo:local  # manually build the docker image (optional)\nexport ANTHROPIC_API_KEY=%your_api_key%\ndocker run \\\n    -e ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \\\n    -v $(pwd)/computer_use_demo:/home/computeruse/computer_use_demo/ `# mount local python module for development` \\\n    -v $HOME/.anthropic:/home/computeruse/.anthropic \\\n    -p 5900:5900 \\\n    -p 8501:8501 \\\n    -p 6080:6080 \\\n    -p 8080:8080 \\\n    -it computer-use-demo:local  # can also use ghcr.io/anthropics/anthropic-quickstarts:computer-use-demo-latest\n```\n\nThe docker run command above mounts the repo inside the docker image, such that you can edit files from the host. Streamlit is already configured with auto reloading.\n"
  },
  {
    "path": "computer-use-demo/computer_use_demo/__init__.py",
    "content": ""
  },
  {
    "path": "computer-use-demo/computer_use_demo/loop.py",
    "content": "\"\"\"\nAgentic sampling loop that calls the Claude API and local implementation of anthropic-defined computer use tools.\n\"\"\"\n\nimport platform\nfrom collections.abc import Callable\nfrom datetime import datetime\nfrom enum import StrEnum\nfrom typing import Any, cast\n\nimport httpx\nfrom anthropic import (\n    Anthropic,\n    AnthropicBedrock,\n    AnthropicVertex,\n    APIError,\n    APIResponseValidationError,\n    APIStatusError,\n)\nfrom anthropic.types.beta import (\n    BetaCacheControlEphemeralParam,\n    BetaContentBlockParam,\n    BetaImageBlockParam,\n    BetaMessage,\n    BetaMessageParam,\n    BetaTextBlock,\n    BetaTextBlockParam,\n    BetaToolResultBlockParam,\n    BetaToolUseBlockParam,\n)\n\nfrom .tools import (\n    TOOL_GROUPS_BY_VERSION,\n    ToolCollection,\n    ToolResult,\n    ToolVersion,\n)\n\nPROMPT_CACHING_BETA_FLAG = \"prompt-caching-2024-07-31\"\n\n\nclass APIProvider(StrEnum):\n    ANTHROPIC = \"anthropic\"\n    BEDROCK = \"bedrock\"\n    VERTEX = \"vertex\"\n\n\n# This system prompt is optimized for the Docker environment in this repository and\n# specific tool combinations enabled.\n# We encourage modifying this system prompt to ensure the model has context for the\n# environment it is running in, and to provide any additional information that may be\n# helpful for the task at hand.\nSYSTEM_PROMPT = f\"\"\"<SYSTEM_CAPABILITY>\n* You are utilising an Ubuntu virtual machine using {platform.machine()} architecture with internet access.\n* You can feel free to install Ubuntu applications with your bash tool. Use curl instead of wget.\n* To open firefox, please just click on the firefox icon.  Note, firefox-esr is what is installed on your system.\n* Using bash tool you can start GUI applications, but you need to set export DISPLAY=:1 and use a subshell. For example \"(DISPLAY=:1 xterm &)\". GUI apps run with bash tool will appear within your desktop environment, but they may take some time to appear. Take a screenshot to confirm it did.\n* When using your bash tool with commands that are expected to output very large quantities of text, redirect into a tmp file and use str_replace_based_edit_tool or `grep -n -B <lines before> -A <lines after> <query> <filename>` to confirm output.\n* When viewing a page it can be helpful to zoom out so that you can see everything on the page.  Either that, or make sure you scroll down to see everything before deciding something isn't available.\n* When using your computer function calls, they take a while to run and send back to you.  Where possible/feasible, try to chain multiple of these calls all into one function calls request.\n* The current date is {datetime.today().strftime(\"%A, %B %-d, %Y\")}.\n</SYSTEM_CAPABILITY>\n\n<IMPORTANT>\n* When using Firefox, if a startup wizard appears, IGNORE IT.  Do not even click \"skip this step\".  Instead, click on the address bar where it says \"Search or enter address\", and enter the appropriate search term or URL there.\n* If the item you are looking at is a pdf, if after taking a single screenshot of the pdf it seems that you want to read the entire document instead of trying to continue to read the pdf from your screenshots + navigation, determine the URL, use curl to download the pdf, install and use pdftotext to convert it to a text file, and then read that text file directly with your str_replace_based_edit_tool.\n</IMPORTANT>\"\"\"\n\n\nasync def sampling_loop(\n    *,\n    model: str,\n    provider: APIProvider,\n    system_prompt_suffix: str,\n    messages: list[BetaMessageParam],\n    output_callback: Callable[[BetaContentBlockParam], None],\n    tool_output_callback: Callable[[ToolResult, str], None],\n    api_response_callback: Callable[\n        [httpx.Request, httpx.Response | object | None, Exception | None], None\n    ],\n    api_key: str,\n    only_n_most_recent_images: int | None = None,\n    max_tokens: int = 4096,\n    tool_version: ToolVersion,\n    thinking_budget: int | None = None,\n    token_efficient_tools_beta: bool = False,\n):\n    \"\"\"\n    Agentic sampling loop for the assistant/tool interaction of computer use.\n    \"\"\"\n    tool_group = TOOL_GROUPS_BY_VERSION[tool_version]\n    tool_collection = ToolCollection(*(ToolCls() for ToolCls in tool_group.tools))\n    system = BetaTextBlockParam(\n        type=\"text\",\n        text=f\"{SYSTEM_PROMPT}{' ' + system_prompt_suffix if system_prompt_suffix else ''}\",\n    )\n\n    while True:\n        enable_prompt_caching = False\n        betas = [tool_group.beta_flag] if tool_group.beta_flag else []\n        if token_efficient_tools_beta:\n            betas.append(\"token-efficient-tools-2025-02-19\")\n        image_truncation_threshold = only_n_most_recent_images or 0\n        if provider == APIProvider.ANTHROPIC:\n            client = Anthropic(api_key=api_key, max_retries=4)\n            enable_prompt_caching = True\n        elif provider == APIProvider.VERTEX:\n            client = AnthropicVertex()\n        elif provider == APIProvider.BEDROCK:\n            client = AnthropicBedrock()\n\n        if enable_prompt_caching:\n            betas.append(PROMPT_CACHING_BETA_FLAG)\n            _inject_prompt_caching(messages)\n            # Because cached reads are 10% of the price, we don't think it's\n            # ever sensible to break the cache by truncating images\n            only_n_most_recent_images = 0\n            # Use type ignore to bypass TypedDict check until SDK types are updated\n            system[\"cache_control\"] = {\"type\": \"ephemeral\"}  # type: ignore\n\n        if only_n_most_recent_images:\n            _maybe_filter_to_n_most_recent_images(\n                messages,\n                only_n_most_recent_images,\n                min_removal_threshold=image_truncation_threshold,\n            )\n        extra_body = {}\n        if thinking_budget:\n            # Ensure we only send the required fields for thinking\n            extra_body = {\n                \"thinking\": {\"type\": \"enabled\", \"budget_tokens\": thinking_budget}\n            }\n\n        # Call the API\n        # we use raw_response to provide debug information to streamlit. Your\n        # implementation may be able call the SDK directly with:\n        # `response = client.messages.create(...)` instead.\n        try:\n            raw_response = client.beta.messages.with_raw_response.create(\n                max_tokens=max_tokens,\n                messages=messages,\n                model=model,\n                system=[system],\n                tools=tool_collection.to_params(),\n                betas=betas,\n                extra_body=extra_body,\n            )\n        except (APIStatusError, APIResponseValidationError) as e:\n            api_response_callback(e.request, e.response, e)\n            return messages\n        except APIError as e:\n            api_response_callback(e.request, e.body, e)\n            return messages\n\n        api_response_callback(\n            raw_response.http_response.request, raw_response.http_response, None\n        )\n\n        response = raw_response.parse()\n\n        response_params = _response_to_params(response)\n        messages.append(\n            {\n                \"role\": \"assistant\",\n                \"content\": response_params,\n            }\n        )\n\n        tool_result_content: list[BetaToolResultBlockParam] = []\n        for content_block in response_params:\n            output_callback(content_block)\n            if (\n                isinstance(content_block, dict)\n                and content_block.get(\"type\") == \"tool_use\"\n            ):\n                # Type narrowing for tool use blocks\n                tool_use_block = cast(BetaToolUseBlockParam, content_block)\n                result = await tool_collection.run(\n                    name=tool_use_block[\"name\"],\n                    tool_input=cast(dict[str, Any], tool_use_block.get(\"input\", {})),\n                )\n                tool_result_content.append(\n                    _make_api_tool_result(result, tool_use_block[\"id\"])\n                )\n                tool_output_callback(result, tool_use_block[\"id\"])\n\n        if not tool_result_content:\n            return messages\n\n        messages.append({\"content\": tool_result_content, \"role\": \"user\"})\n\n\ndef _maybe_filter_to_n_most_recent_images(\n    messages: list[BetaMessageParam],\n    images_to_keep: int,\n    min_removal_threshold: int,\n):\n    \"\"\"\n    With the assumption that images are screenshots that are of diminishing value as\n    the conversation progresses, remove all but the final `images_to_keep` tool_result\n    images in place, with a chunk of min_removal_threshold to reduce the amount we\n    break the implicit prompt cache.\n    \"\"\"\n    if images_to_keep is None:\n        return messages\n\n    tool_result_blocks = cast(\n        list[BetaToolResultBlockParam],\n        [\n            item\n            for message in messages\n            for item in (\n                message[\"content\"] if isinstance(message[\"content\"], list) else []\n            )\n            if isinstance(item, dict) and item.get(\"type\") == \"tool_result\"\n        ],\n    )\n\n    total_images = sum(\n        1\n        for tool_result in tool_result_blocks\n        for content in tool_result.get(\"content\", [])\n        if isinstance(content, dict) and content.get(\"type\") == \"image\"\n    )\n\n    images_to_remove = total_images - images_to_keep\n    # for better cache behavior, we want to remove in chunks\n    images_to_remove -= images_to_remove % min_removal_threshold\n\n    for tool_result in tool_result_blocks:\n        if isinstance(tool_result.get(\"content\"), list):\n            new_content = []\n            for content in tool_result.get(\"content\", []):\n                if isinstance(content, dict) and content.get(\"type\") == \"image\":\n                    if images_to_remove > 0:\n                        images_to_remove -= 1\n                        continue\n                new_content.append(content)\n            tool_result[\"content\"] = new_content\n\n\ndef _response_to_params(\n    response: BetaMessage,\n) -> list[BetaContentBlockParam]:\n    res: list[BetaContentBlockParam] = []\n    for block in response.content:\n        if isinstance(block, BetaTextBlock):\n            if block.text:\n                res.append(BetaTextBlockParam(type=\"text\", text=block.text))\n            elif getattr(block, \"type\", None) == \"thinking\":\n                # Handle thinking blocks - include signature field\n                thinking_block = {\n                    \"type\": \"thinking\",\n                    \"thinking\": getattr(block, \"thinking\", None),\n                }\n                if hasattr(block, \"signature\"):\n                    thinking_block[\"signature\"] = getattr(block, \"signature\", None)\n                res.append(cast(BetaContentBlockParam, thinking_block))\n        else:\n            # Handle tool use blocks normally\n            res.append(cast(BetaToolUseBlockParam, block.model_dump()))\n    return res\n\n\ndef _inject_prompt_caching(\n    messages: list[BetaMessageParam],\n):\n    \"\"\"\n    Set cache breakpoints for the 3 most recent turns\n    one cache breakpoint is left for tools/system prompt, to be shared across sessions\n    \"\"\"\n\n    breakpoints_remaining = 3\n    for message in reversed(messages):\n        if message[\"role\"] == \"user\" and isinstance(\n            content := message[\"content\"], list\n        ):\n            if breakpoints_remaining:\n                breakpoints_remaining -= 1\n                # Use type ignore to bypass TypedDict check until SDK types are updated\n                content[-1][\"cache_control\"] = BetaCacheControlEphemeralParam(  # type: ignore\n                    {\"type\": \"ephemeral\"}\n                )\n            else:\n                if isinstance(content[-1], dict) and \"cache_control\" in content[-1]:\n                    del content[-1][\"cache_control\"]  # type: ignore\n                # we'll only every have one extra turn per loop\n                break\n\n\ndef _make_api_tool_result(\n    result: ToolResult, tool_use_id: str\n) -> BetaToolResultBlockParam:\n    \"\"\"Convert an agent ToolResult to an API ToolResultBlockParam.\"\"\"\n    tool_result_content: list[BetaTextBlockParam | BetaImageBlockParam] | str = []\n    is_error = False\n    if result.error:\n        is_error = True\n        tool_result_content = _maybe_prepend_system_tool_result(result, result.error)\n    else:\n        if result.output:\n            tool_result_content.append(\n                {\n                    \"type\": \"text\",\n                    \"text\": _maybe_prepend_system_tool_result(result, result.output),\n                }\n            )\n        if result.base64_image:\n            tool_result_content.append(\n                {\n                    \"type\": \"image\",\n                    \"source\": {\n                        \"type\": \"base64\",\n                        \"media_type\": \"image/png\",\n                        \"data\": result.base64_image,\n                    },\n                }\n            )\n    return {\n        \"type\": \"tool_result\",\n        \"content\": tool_result_content,\n        \"tool_use_id\": tool_use_id,\n        \"is_error\": is_error,\n    }\n\n\ndef _maybe_prepend_system_tool_result(result: ToolResult, result_text: str):\n    if result.system:\n        result_text = f\"<system>{result.system}</system>\\n{result_text}\"\n    return result_text\n"
  },
  {
    "path": "computer-use-demo/computer_use_demo/requirements.txt",
    "content": "streamlit==1.41.0\nanthropic[bedrock,vertex]>=0.39.0\njsonschema==4.22.0\nboto3>=1.28.57\ngoogle-auth<3,>=2\n"
  },
  {
    "path": "computer-use-demo/computer_use_demo/streamlit.py",
    "content": "\"\"\"\nEntrypoint for streamlit, see https://docs.streamlit.io/\n\"\"\"\n\nimport asyncio\nimport base64\nimport os\nimport subprocess\nimport traceback\nfrom contextlib import contextmanager\nfrom dataclasses import dataclass\nfrom datetime import datetime, timedelta\nfrom enum import StrEnum\nfrom functools import partial\nfrom pathlib import PosixPath\nfrom typing import cast, get_args\n\nimport httpx\nimport streamlit as st\nfrom anthropic import RateLimitError\nfrom anthropic.types.beta import (\n    BetaContentBlockParam,\n    BetaTextBlockParam,\n    BetaToolResultBlockParam,\n)\nfrom streamlit.delta_generator import DeltaGenerator\n\nfrom computer_use_demo.loop import (\n    APIProvider,\n    sampling_loop,\n)\nfrom computer_use_demo.tools import ToolResult, ToolVersion\n\nPROVIDER_TO_DEFAULT_MODEL_NAME: dict[APIProvider, str] = {\n    APIProvider.ANTHROPIC: \"claude-sonnet-4-5-20250929\",\n    APIProvider.BEDROCK: \"anthropic.claude-3-5-sonnet-20241022-v2:0\",\n    APIProvider.VERTEX: \"claude-3-5-sonnet-v2@20241022\",\n}\n\n\n@dataclass(kw_only=True, frozen=True)\nclass ModelConfig:\n    tool_version: ToolVersion\n    max_output_tokens: int\n    default_output_tokens: int\n    has_thinking: bool = False\n\n\nCLAUDE_4 = ModelConfig(\n    tool_version=\"computer_use_20250429\",\n    max_output_tokens=64_000,\n    default_output_tokens=1024 * 16,\n    has_thinking=True,\n)\n\nCLAUDE_4_5 = ModelConfig(\n    tool_version=\"computer_use_20250124\",\n    max_output_tokens=128_000,\n    default_output_tokens=1024 * 16,\n    has_thinking=True,\n)\n\nCLAUDE_4_WITH_ZOOMABLE_TOOL = ModelConfig(\n    tool_version=\"computer_use_20251124\",\n    max_output_tokens=64_000,\n    default_output_tokens=1024 * 16,\n    has_thinking=True,\n)\n\nHAIKU_4_5 = ModelConfig(\n    tool_version=\"computer_use_20250124\",\n    max_output_tokens=1024 * 8,\n    default_output_tokens=1024 * 4,\n    has_thinking=False,\n)\n\nMODEL_TO_MODEL_CONF: dict[str, ModelConfig] = {\n    \"claude-opus-4-1-20250805\": CLAUDE_4,\n    \"claude-sonnet-4-20250514\": CLAUDE_4,\n    \"claude-opus-4-20250514\": CLAUDE_4,\n    \"claude-sonnet-4-5-20250929\": CLAUDE_4_5,\n    \"anthropic.claude-sonnet-4-5-20250929-v1:0\": CLAUDE_4_5,\n    \"claude-sonnet-4-5@20250929\": CLAUDE_4_5,\n    \"claude-haiku-4-5-20251001\": HAIKU_4_5,\n    \"anthropic.claude-haiku-4-5-20251001-v1:0\": HAIKU_4_5,  # Bedrock\n    \"claude-haiku-4-5@20251001\": HAIKU_4_5,  # Vertex\n    \"claude-opus-4-5-20251101\": CLAUDE_4_WITH_ZOOMABLE_TOOL,\n}\n\nCONFIG_DIR = PosixPath(\"~/.anthropic\").expanduser()\nAPI_KEY_FILE = CONFIG_DIR / \"api_key\"\nSTREAMLIT_STYLE = \"\"\"\n<style>\n    /* Highlight the stop button in red */\n    button[kind=header] {\n        background-color: rgb(255, 75, 75);\n        border: 1px solid rgb(255, 75, 75);\n        color: rgb(255, 255, 255);\n    }\n    button[kind=header]:hover {\n        background-color: rgb(255, 51, 51);\n    }\n     /* Hide the streamlit deploy button */\n    .stAppDeployButton {\n        visibility: hidden;\n    }\n</style>\n\"\"\"\n\nWARNING_TEXT = \"⚠️ Security Alert: Never provide access to sensitive accounts or data, as malicious web content can hijack Claude's behavior\"\nINTERRUPT_TEXT = \"(user stopped or interrupted and wrote the following)\"\nINTERRUPT_TOOL_ERROR = \"human stopped or interrupted tool execution\"\n\n\nclass Sender(StrEnum):\n    USER = \"user\"\n    BOT = \"assistant\"\n    TOOL = \"tool\"\n\n\ndef setup_state():\n    if \"messages\" not in st.session_state:\n        st.session_state.messages = []\n    if \"api_key\" not in st.session_state:\n        # Try to load API key from file first, then environment\n        st.session_state.api_key = load_from_storage(\"api_key\") or os.getenv(\n            \"ANTHROPIC_API_KEY\", \"\"\n        )\n    if \"provider\" not in st.session_state:\n        st.session_state.provider = (\n            os.getenv(\"API_PROVIDER\", \"anthropic\") or APIProvider.ANTHROPIC\n        )\n    if \"provider_radio\" not in st.session_state:\n        st.session_state.provider_radio = st.session_state.provider\n    if \"model\" not in st.session_state:\n        _reset_model()\n    if \"auth_validated\" not in st.session_state:\n        st.session_state.auth_validated = False\n    if \"responses\" not in st.session_state:\n        st.session_state.responses = {}\n    if \"tools\" not in st.session_state:\n        st.session_state.tools = {}\n    if \"only_n_most_recent_images\" not in st.session_state:\n        st.session_state.only_n_most_recent_images = 3\n    if \"custom_system_prompt\" not in st.session_state:\n        st.session_state.custom_system_prompt = load_from_storage(\"system_prompt\") or \"\"\n    if \"hide_images\" not in st.session_state:\n        st.session_state.hide_images = False\n    if \"token_efficient_tools_beta\" not in st.session_state:\n        st.session_state.token_efficient_tools_beta = False\n    if \"in_sampling_loop\" not in st.session_state:\n        st.session_state.in_sampling_loop = False\n\n\ndef _reset_model():\n    st.session_state.model = PROVIDER_TO_DEFAULT_MODEL_NAME[\n        cast(APIProvider, st.session_state.provider)\n    ]\n    _reset_model_conf()\n\n\ndef _reset_model_conf():\n    model_conf = (\n        MODEL_TO_MODEL_CONF.get(st.session_state.model, CLAUDE_4)  # Default fallback\n    )\n\n    # If we're in radio selection mode, use the selected tool version\n    if hasattr(st.session_state, \"tool_versions\"):\n        st.session_state.tool_version = st.session_state.tool_versions\n    else:\n        st.session_state.tool_version = model_conf.tool_version\n\n    st.session_state.has_thinking = model_conf.has_thinking\n    st.session_state.output_tokens = model_conf.default_output_tokens\n    st.session_state.max_output_tokens = model_conf.max_output_tokens\n    st.session_state.thinking_budget = int(model_conf.default_output_tokens / 2)\n\n\nasync def main():\n    \"\"\"Render loop for streamlit\"\"\"\n    setup_state()\n\n    st.markdown(STREAMLIT_STYLE, unsafe_allow_html=True)\n\n    st.title(\"Claude Computer Use Demo\")\n\n    if not os.getenv(\"HIDE_WARNING\", False):\n        st.warning(WARNING_TEXT)\n\n    with st.sidebar:\n\n        def _reset_api_provider():\n            if st.session_state.provider_radio != st.session_state.provider:\n                _reset_model()\n                st.session_state.provider = st.session_state.provider_radio\n                st.session_state.auth_validated = False\n\n        provider_options = [option.value for option in APIProvider]\n        st.radio(\n            \"API Provider\",\n            options=provider_options,\n            key=\"provider_radio\",\n            format_func=lambda x: x.title(),\n            on_change=_reset_api_provider,\n        )\n\n        st.text_input(\"Model\", key=\"model\", on_change=_reset_model_conf)\n\n        if st.session_state.provider == APIProvider.ANTHROPIC:\n            st.text_input(\n                \"Claude API Key\",\n                type=\"password\",\n                key=\"api_key\",\n                on_change=lambda: save_to_storage(\"api_key\", st.session_state.api_key),\n            )\n\n        st.number_input(\n            \"Only send N most recent images\",\n            min_value=0,\n            key=\"only_n_most_recent_images\",\n            help=\"To decrease the total tokens sent, remove older screenshots from the conversation\",\n        )\n        st.text_area(\n            \"Custom System Prompt Suffix\",\n            key=\"custom_system_prompt\",\n            help=\"Additional instructions to append to the system prompt. see computer_use_demo/loop.py for the base system prompt.\",\n            on_change=lambda: save_to_storage(\n                \"system_prompt\", st.session_state.custom_system_prompt\n            ),\n        )\n        st.checkbox(\"Hide screenshots\", key=\"hide_images\")\n        st.checkbox(\n            \"Enable token-efficient tools beta\", key=\"token_efficient_tools_beta\"\n        )\n        versions = get_args(ToolVersion)\n        st.radio(\n            \"Tool Versions\",\n            key=\"tool_versions\",\n            options=versions,\n            index=versions.index(st.session_state.tool_version),\n            on_change=lambda: setattr(\n                st.session_state,\n                \"tool_version\",\n                st.session_state.get(\"tool_versions\", st.session_state.tool_version),\n            ),\n        )\n\n        st.number_input(\"Max Output Tokens\", key=\"output_tokens\", step=1)\n\n        st.checkbox(\"Thinking Enabled\", key=\"thinking\", value=False)\n        st.number_input(\n            \"Thinking Budget\",\n            key=\"thinking_budget\",\n            max_value=st.session_state.max_output_tokens,\n            step=1,\n            disabled=not st.session_state.thinking,\n        )\n\n        if st.button(\"Reset\", type=\"primary\"):\n            with st.spinner(\"Resetting...\"):\n                st.session_state.clear()\n                setup_state()\n\n                subprocess.run(\"pkill Xvfb; pkill tint2\", shell=True)  # noqa: ASYNC221\n                await asyncio.sleep(1)\n                subprocess.run(\"./start_all.sh\", shell=True)  # noqa: ASYNC221\n\n    if not st.session_state.auth_validated:\n        if auth_error := validate_auth(\n            st.session_state.provider, st.session_state.api_key\n        ):\n            st.warning(f\"Please resolve the following auth issue:\\n\\n{auth_error}\")\n            return\n        else:\n            st.session_state.auth_validated = True\n\n    chat, http_logs = st.tabs([\"Chat\", \"HTTP Exchange Logs\"])\n    new_message = st.chat_input(\n        \"Type a message to send to Claude to control the computer...\"\n    )\n\n    with chat:\n        # render past chats\n        for message in st.session_state.messages:\n            if isinstance(message[\"content\"], str):\n                _render_message(message[\"role\"], message[\"content\"])\n            elif isinstance(message[\"content\"], list):\n                for block in message[\"content\"]:\n                    # the tool result we send back to the Claude API isn't sufficient to render all details,\n                    # so we store the tool use responses\n                    if isinstance(block, dict) and block[\"type\"] == \"tool_result\":\n                        _render_message(\n                            Sender.TOOL, st.session_state.tools[block[\"tool_use_id\"]]\n                        )\n                    else:\n                        _render_message(\n                            message[\"role\"],\n                            cast(BetaContentBlockParam | ToolResult, block),\n                        )\n\n        # render past http exchanges\n        for identity, (request, response) in st.session_state.responses.items():\n            _render_api_response(request, response, identity, http_logs)\n\n        # render past chats\n        if new_message:\n            st.session_state.messages.append(\n                {\n                    \"role\": Sender.USER,\n                    \"content\": [\n                        *maybe_add_interruption_blocks(),\n                        BetaTextBlockParam(type=\"text\", text=new_message),\n                    ],\n                }\n            )\n            _render_message(Sender.USER, new_message)\n\n        try:\n            most_recent_message = st.session_state[\"messages\"][-1]\n        except IndexError:\n            return\n\n        if most_recent_message[\"role\"] is not Sender.USER:\n            # we don't have a user message to respond to, exit early\n            return\n\n        with track_sampling_loop():\n            # run the agent sampling loop with the newest message\n            st.session_state.messages = await sampling_loop(\n                system_prompt_suffix=st.session_state.custom_system_prompt,\n                model=st.session_state.model,\n                provider=st.session_state.provider,\n                messages=st.session_state.messages,\n                output_callback=partial(_render_message, Sender.BOT),\n                tool_output_callback=partial(\n                    _tool_output_callback, tool_state=st.session_state.tools\n                ),\n                api_response_callback=partial(\n                    _api_response_callback,\n                    tab=http_logs,\n                    response_state=st.session_state.responses,\n                ),\n                api_key=st.session_state.api_key,\n                only_n_most_recent_images=st.session_state.only_n_most_recent_images,\n                tool_version=st.session_state.tool_versions,\n                max_tokens=st.session_state.output_tokens,\n                thinking_budget=st.session_state.thinking_budget\n                if st.session_state.thinking\n                else None,\n                token_efficient_tools_beta=st.session_state.token_efficient_tools_beta,\n            )\n\n\ndef maybe_add_interruption_blocks():\n    if not st.session_state.in_sampling_loop:\n        return []\n    # If this function is called while we're in the sampling loop, we can assume that the previous sampling loop was interrupted\n    # and we should annotate the conversation with additional context for the model and heal any incomplete tool use calls\n    result = []\n    last_message = st.session_state.messages[-1]\n    previous_tool_use_ids = [\n        block[\"id\"] for block in last_message[\"content\"] if block[\"type\"] == \"tool_use\"\n    ]\n    for tool_use_id in previous_tool_use_ids:\n        st.session_state.tools[tool_use_id] = ToolResult(error=INTERRUPT_TOOL_ERROR)\n        result.append(\n            BetaToolResultBlockParam(\n                tool_use_id=tool_use_id,\n                type=\"tool_result\",\n                content=INTERRUPT_TOOL_ERROR,\n                is_error=True,\n            )\n        )\n    result.append(BetaTextBlockParam(type=\"text\", text=INTERRUPT_TEXT))\n    return result\n\n\n@contextmanager\ndef track_sampling_loop():\n    st.session_state.in_sampling_loop = True\n    yield\n    st.session_state.in_sampling_loop = False\n\n\ndef validate_auth(provider: APIProvider, api_key: str | None):\n    if provider == APIProvider.ANTHROPIC:\n        if not api_key:\n            return \"Enter your Claude API key in the sidebar to continue.\"\n    if provider == APIProvider.BEDROCK:\n        import boto3\n\n        if not boto3.Session().get_credentials():\n            return \"You must have AWS credentials set up to use the Bedrock API.\"\n    if provider == APIProvider.VERTEX:\n        import google.auth\n        from google.auth.exceptions import DefaultCredentialsError\n\n        if not os.environ.get(\"CLOUD_ML_REGION\"):\n            return \"Set the CLOUD_ML_REGION environment variable to use the Vertex API.\"\n        try:\n            google.auth.default(\n                scopes=[\"https://www.googleapis.com/auth/cloud-platform\"],\n            )\n        except DefaultCredentialsError:\n            return \"Your google cloud credentials are not set up correctly.\"\n\n\ndef load_from_storage(filename: str) -> str | None:\n    \"\"\"Load data from a file in the storage directory.\"\"\"\n    try:\n        file_path = CONFIG_DIR / filename\n        if file_path.exists():\n            data = file_path.read_text().strip()\n            if data:\n                return data\n    except Exception as e:\n        st.write(f\"Debug: Error loading {filename}: {e}\")\n    return None\n\n\ndef save_to_storage(filename: str, data: str) -> None:\n    \"\"\"Save data to a file in the storage directory.\"\"\"\n    try:\n        CONFIG_DIR.mkdir(parents=True, exist_ok=True)\n        file_path = CONFIG_DIR / filename\n        file_path.write_text(data)\n        # Ensure only user can read/write the file\n        file_path.chmod(0o600)\n    except Exception as e:\n        st.write(f\"Debug: Error saving {filename}: {e}\")\n\n\ndef _api_response_callback(\n    request: httpx.Request,\n    response: httpx.Response | object | None,\n    error: Exception | None,\n    tab: DeltaGenerator,\n    response_state: dict[str, tuple[httpx.Request, httpx.Response | object | None]],\n):\n    \"\"\"\n    Handle an API response by storing it to state and rendering it.\n    \"\"\"\n    response_id = datetime.now().isoformat()\n    response_state[response_id] = (request, response)\n    if error:\n        _render_error(error)\n    _render_api_response(request, response, response_id, tab)\n\n\ndef _tool_output_callback(\n    tool_output: ToolResult, tool_id: str, tool_state: dict[str, ToolResult]\n):\n    \"\"\"Handle a tool output by storing it to state and rendering it.\"\"\"\n    tool_state[tool_id] = tool_output\n    _render_message(Sender.TOOL, tool_output)\n\n\ndef _render_api_response(\n    request: httpx.Request,\n    response: httpx.Response | object | None,\n    response_id: str,\n    tab: DeltaGenerator,\n):\n    \"\"\"Render an API response to a streamlit tab\"\"\"\n    with tab:\n        with st.expander(f\"Request/Response ({response_id})\"):\n            newline = \"\\n\\n\"\n            st.markdown(\n                f\"`{request.method} {request.url}`{newline}{newline.join(f'`{k}: {v}`' for k, v in request.headers.items())}\"\n            )\n            st.json(request.read().decode())\n            st.markdown(\"---\")\n            if isinstance(response, httpx.Response):\n                st.markdown(\n                    f\"`{response.status_code}`{newline}{newline.join(f'`{k}: {v}`' for k, v in response.headers.items())}\"\n                )\n                st.json(response.text)\n            else:\n                st.write(response)\n\n\ndef _render_error(error: Exception):\n    if isinstance(error, RateLimitError):\n        body = \"You have been rate limited.\"\n        if retry_after := error.response.headers.get(\"retry-after\"):\n            body += f\" **Retry after {str(timedelta(seconds=int(retry_after)))} (HH:MM:SS).** See our API [documentation](https://docs.claude.com/en/api/rate-limits) for more details.\"\n        body += f\"\\n\\n{error.message}\"\n    else:\n        body = str(error)\n        body += \"\\n\\n**Traceback:**\"\n        lines = \"\\n\".join(traceback.format_exception(error))\n        body += f\"\\n\\n```{lines}```\"\n    save_to_storage(f\"error_{datetime.now().timestamp()}.md\", body)\n    st.error(f\"**{error.__class__.__name__}**\\n\\n{body}\", icon=\":material/error:\")\n\n\ndef _render_message(\n    sender: Sender,\n    message: str | BetaContentBlockParam | ToolResult,\n):\n    \"\"\"Convert input from the user or output from the agent to a streamlit message.\"\"\"\n    # streamlit's hotreloading breaks isinstance checks, so we need to check for class names\n    is_tool_result = not isinstance(message, str | dict)\n    if not message or (\n        is_tool_result\n        and st.session_state.hide_images\n        and not hasattr(message, \"error\")\n        and not hasattr(message, \"output\")\n    ):\n        return\n    with st.chat_message(sender):\n        if is_tool_result:\n            message = cast(ToolResult, message)\n            if message.output:\n                if message.__class__.__name__ == \"CLIResult\":\n                    st.code(message.output)\n                else:\n                    st.markdown(message.output)\n            if message.error:\n                st.error(message.error)\n            if message.base64_image and not st.session_state.hide_images:\n                st.image(base64.b64decode(message.base64_image))\n        elif isinstance(message, dict):\n            if message[\"type\"] == \"text\":\n                st.write(message[\"text\"])\n            elif message[\"type\"] == \"thinking\":\n                thinking_content = message.get(\"thinking\", \"\")\n                st.markdown(f\"[Thinking]\\n\\n{thinking_content}\")\n            elif message[\"type\"] == \"tool_use\":\n                st.code(f\"Tool Use: {message['name']}\\nInput: {message['input']}\")\n            else:\n                # only expected return types are text and tool_use\n                raise Exception(f\"Unexpected response type {message['type']}\")\n        else:\n            st.markdown(message)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "computer-use-demo/computer_use_demo/tools/__init__.py",
    "content": "from .base import CLIResult, ToolResult\nfrom .bash import BashTool20241022, BashTool20250124\nfrom .collection import ToolCollection\nfrom .computer import ComputerTool20241022, ComputerTool20250124\nfrom .edit import EditTool20250728\nfrom .groups import TOOL_GROUPS_BY_VERSION, ToolVersion\n\n__ALL__ = [\n    BashTool20241022,\n    BashTool20250124,\n    CLIResult,\n    ComputerTool20241022,\n    ComputerTool20250124,\n    EditTool20250728,\n    TOOL_GROUPS_BY_VERSION,\n    ToolCollection,\n    ToolResult,\n    ToolVersion,\n]\n"
  },
  {
    "path": "computer-use-demo/computer_use_demo/tools/base.py",
    "content": "from abc import ABCMeta, abstractmethod\nfrom dataclasses import dataclass, fields, replace\nfrom typing import Any\n\nfrom anthropic.types.beta import BetaToolUnionParam\n\n\nclass BaseAnthropicTool(metaclass=ABCMeta):\n    \"\"\"Abstract base class for Anthropic-defined tools.\"\"\"\n\n    @abstractmethod\n    def __call__(self, **kwargs) -> Any:\n        \"\"\"Executes the tool with the given arguments.\"\"\"\n        ...\n\n    @abstractmethod\n    def to_params(\n        self,\n    ) -> BetaToolUnionParam:\n        raise NotImplementedError\n\n\n@dataclass(kw_only=True, frozen=True)\nclass ToolResult:\n    \"\"\"Represents the result of a tool execution.\"\"\"\n\n    output: str | None = None\n    error: str | None = None\n    base64_image: str | None = None\n    system: str | None = None\n\n    def __bool__(self):\n        return any(getattr(self, field.name) for field in fields(self))\n\n    def __add__(self, other: \"ToolResult\"):\n        def combine_fields(\n            field: str | None, other_field: str | None, concatenate: bool = True\n        ):\n            if field and other_field:\n                if concatenate:\n                    return field + other_field\n                raise ValueError(\"Cannot combine tool results\")\n            return field or other_field\n\n        return ToolResult(\n            output=combine_fields(self.output, other.output),\n            error=combine_fields(self.error, other.error),\n            base64_image=combine_fields(self.base64_image, other.base64_image, False),\n            system=combine_fields(self.system, other.system),\n        )\n\n    def replace(self, **kwargs):\n        \"\"\"Returns a new ToolResult with the given fields replaced.\"\"\"\n        return replace(self, **kwargs)\n\n\nclass CLIResult(ToolResult):\n    \"\"\"A ToolResult that can be rendered as a CLI output.\"\"\"\n\n\nclass ToolFailure(ToolResult):\n    \"\"\"A ToolResult that represents a failure.\"\"\"\n\n\nclass ToolError(Exception):\n    \"\"\"Raised when a tool encounters an error.\"\"\"\n\n    def __init__(self, message):\n        self.message = message\n"
  },
  {
    "path": "computer-use-demo/computer_use_demo/tools/bash.py",
    "content": "import asyncio\nimport os\nfrom typing import Any, Literal\n\nfrom .base import BaseAnthropicTool, CLIResult, ToolError, ToolResult\n\n\nclass _BashSession:\n    \"\"\"A session of a bash shell.\"\"\"\n\n    _started: bool\n    _process: asyncio.subprocess.Process\n\n    command: str = \"/bin/bash\"\n    _output_delay: float = 0.2  # seconds\n    _timeout: float = 120.0  # seconds\n    _sentinel: str = \"<<exit>>\"\n\n    def __init__(self):\n        self._started = False\n        self._timed_out = False\n\n    async def start(self):\n        if self._started:\n            return\n\n        self._process = await asyncio.create_subprocess_shell(\n            self.command,\n            preexec_fn=os.setsid,\n            shell=True,\n            bufsize=0,\n            stdin=asyncio.subprocess.PIPE,\n            stdout=asyncio.subprocess.PIPE,\n            stderr=asyncio.subprocess.PIPE,\n        )\n\n        self._started = True\n\n    def stop(self):\n        \"\"\"Terminate the bash shell.\"\"\"\n        if not self._started:\n            raise ToolError(\"Session has not started.\")\n        if self._process.returncode is not None:\n            return\n        self._process.terminate()\n\n    async def run(self, command: str):\n        \"\"\"Execute a command in the bash shell.\"\"\"\n        if not self._started:\n            raise ToolError(\"Session has not started.\")\n        if self._process.returncode is not None:\n            return ToolResult(\n                system=\"tool must be restarted\",\n                error=f\"bash has exited with returncode {self._process.returncode}\",\n            )\n        if self._timed_out:\n            raise ToolError(\n                f\"timed out: bash has not returned in {self._timeout} seconds and must be restarted\",\n            )\n\n        # we know these are not None because we created the process with PIPEs\n        assert self._process.stdin\n        assert self._process.stdout\n        assert self._process.stderr\n\n        # send command to the process\n        self._process.stdin.write(\n            command.encode() + f\"; echo '{self._sentinel}'\\n\".encode()\n        )\n        await self._process.stdin.drain()\n\n        # read output from the process, until the sentinel is found\n        try:\n            async with asyncio.timeout(self._timeout):\n                while True:\n                    await asyncio.sleep(self._output_delay)\n                    # if we read directly from stdout/stderr, it will wait forever for\n                    # EOF. use the StreamReader buffer directly instead.\n                    output = self._process.stdout._buffer.decode()  # pyright: ignore[reportAttributeAccessIssue]\n                    if self._sentinel in output:\n                        # strip the sentinel and break\n                        output = output[: output.index(self._sentinel)]\n                        break\n        except asyncio.TimeoutError:\n            self._timed_out = True\n            raise ToolError(\n                f\"timed out: bash has not returned in {self._timeout} seconds and must be restarted\",\n            ) from None\n\n        if output.endswith(\"\\n\"):\n            output = output[:-1]\n\n        error = self._process.stderr._buffer.decode()  # pyright: ignore[reportAttributeAccessIssue]\n        if error.endswith(\"\\n\"):\n            error = error[:-1]\n\n        # clear the buffers so that the next output can be read correctly\n        self._process.stdout._buffer.clear()  # pyright: ignore[reportAttributeAccessIssue]\n        self._process.stderr._buffer.clear()  # pyright: ignore[reportAttributeAccessIssue]\n\n        return CLIResult(output=output, error=error)\n\n\nclass BashTool20250124(BaseAnthropicTool):\n    \"\"\"\n    A tool that allows the agent to run bash commands.\n    The tool parameters are defined by Anthropic and are not editable.\n    \"\"\"\n\n    _session: _BashSession | None\n\n    api_type: Literal[\"bash_20250124\"] = \"bash_20250124\"\n    name: Literal[\"bash\"] = \"bash\"\n\n    def __init__(self):\n        self._session = None\n        super().__init__()\n\n    def to_params(self) -> Any:\n        return {\n            \"type\": self.api_type,\n            \"name\": self.name,\n        }\n\n    async def __call__(\n        self, command: str | None = None, restart: bool = False, **kwargs\n    ):\n        if restart:\n            if self._session:\n                self._session.stop()\n            self._session = _BashSession()\n            await self._session.start()\n\n            return ToolResult(system=\"tool has been restarted.\")\n\n        if self._session is None:\n            self._session = _BashSession()\n            await self._session.start()\n\n        if command is not None:\n            return await self._session.run(command)\n\n        raise ToolError(\"no command provided.\")\n\n\nclass BashTool20241022(BashTool20250124):\n    api_type: Literal[\"bash_20250124\"] = \"bash_20250124\"  # pyright: ignore[reportIncompatibleVariableOverride]\n"
  },
  {
    "path": "computer-use-demo/computer_use_demo/tools/collection.py",
    "content": "\"\"\"Collection classes for managing multiple tools.\"\"\"\n\nfrom typing import Any, cast\n\nfrom anthropic.types.beta import BetaToolUnionParam\n\nfrom .base import (\n    BaseAnthropicTool,\n    ToolError,\n    ToolFailure,\n    ToolResult,\n)\n\n\nclass ToolCollection:\n    \"\"\"A collection of anthropic-defined tools.\"\"\"\n\n    def __init__(self, *tools: BaseAnthropicTool):\n        self.tools = tools\n        self.tool_map = {\n            cast(dict[str, Any], tool.to_params())[\"name\"]: tool for tool in tools\n        }\n\n    def to_params(\n        self,\n    ) -> list[BetaToolUnionParam]:\n        return [tool.to_params() for tool in self.tools]\n\n    async def run(self, *, name: str, tool_input: dict[str, Any]) -> ToolResult:\n        tool = self.tool_map.get(name)\n        if not tool:\n            return ToolFailure(error=f\"Tool {name} is invalid\")\n        try:\n            return await tool(**tool_input)\n        except ToolError as e:\n            return ToolFailure(error=e.message)\n"
  },
  {
    "path": "computer-use-demo/computer_use_demo/tools/computer.py",
    "content": "import asyncio\nimport base64\nimport os\nimport shlex\nimport shutil\nfrom enum import StrEnum\nfrom pathlib import Path\nfrom typing import Literal, TypedDict, cast, get_args\nfrom uuid import uuid4\n\nfrom anthropic.types.beta import BetaToolComputerUse20241022Param, BetaToolUnionParam\n\nfrom .base import BaseAnthropicTool, ToolError, ToolResult\nfrom .run import run\n\nOUTPUT_DIR = \"/tmp/outputs\"\n\nTYPING_DELAY_MS = 12\nTYPING_GROUP_SIZE = 50\n\nAction_20241022 = Literal[\n    \"key\",\n    \"type\",\n    \"mouse_move\",\n    \"left_click\",\n    \"left_click_drag\",\n    \"right_click\",\n    \"middle_click\",\n    \"double_click\",\n    \"screenshot\",\n    \"cursor_position\",\n]\n\nAction_20250124 = (\n    Action_20241022\n    | Literal[\n        \"left_mouse_down\",\n        \"left_mouse_up\",\n        \"scroll\",\n        \"hold_key\",\n        \"wait\",\n        \"triple_click\",\n    ]\n)\n\nAction_20251124 = Action_20250124 | Literal[\"zoom\"]\n\nScrollDirection = Literal[\"up\", \"down\", \"left\", \"right\"]\n\n\nclass Resolution(TypedDict):\n    width: int\n    height: int\n\n\n# sizes above XGA/WXGA are not recommended (see README.md)\n# scale down to one of these targets if ComputerTool._scaling_enabled is set\nMAX_SCALING_TARGETS: dict[str, Resolution] = {\n    \"XGA\": Resolution(width=1024, height=768),  # 4:3\n    \"WXGA\": Resolution(width=1280, height=800),  # 16:10\n    \"FWXGA\": Resolution(width=1366, height=768),  # ~16:9\n}\n\nCLICK_BUTTONS = {\n    \"left_click\": 1,\n    \"right_click\": 3,\n    \"middle_click\": 2,\n    \"double_click\": \"--repeat 2 --delay 10 1\",\n    \"triple_click\": \"--repeat 3 --delay 10 1\",\n}\n\n\nclass ScalingSource(StrEnum):\n    COMPUTER = \"computer\"\n    API = \"api\"\n\n\nclass ComputerToolOptions(TypedDict):\n    display_height_px: int\n    display_width_px: int\n    display_number: int | None\n\n\ndef chunks(s: str, chunk_size: int) -> list[str]:\n    return [s[i : i + chunk_size] for i in range(0, len(s), chunk_size)]\n\n\nclass BaseComputerTool:\n    \"\"\"\n    A tool that allows the agent to interact with the screen, keyboard, and mouse of the current computer.\n    The tool parameters are defined by Anthropic and are not editable.\n    \"\"\"\n\n    name: Literal[\"computer\"] = \"computer\"\n    width: int\n    height: int\n    display_num: int | None\n\n    _screenshot_delay = 2.0\n    _scaling_enabled = True\n\n    @property\n    def options(self) -> ComputerToolOptions:\n        width, height = self.scale_coordinates(\n            ScalingSource.COMPUTER, self.width, self.height\n        )\n        return {\n            \"display_width_px\": width,\n            \"display_height_px\": height,\n            \"display_number\": self.display_num,\n        }\n\n    def __init__(self):\n        super().__init__()\n\n        self.width = int(os.getenv(\"WIDTH\") or 0)\n        self.height = int(os.getenv(\"HEIGHT\") or 0)\n        assert self.width and self.height, \"WIDTH, HEIGHT must be set\"\n        if (display_num := os.getenv(\"DISPLAY_NUM\")) is not None:\n            self.display_num = int(display_num)\n            self._display_prefix = f\"DISPLAY=:{self.display_num} \"\n        else:\n            self.display_num = None\n            self._display_prefix = \"\"\n\n        self.xdotool = f\"{self._display_prefix}xdotool\"\n\n    async def __call__(\n        self,\n        *,\n        action: Action_20241022,\n        text: str | None = None,\n        coordinate: tuple[int, int] | None = None,\n        start_coordinate: tuple[int, int] | None = None,\n        **kwargs,\n    ):\n        if action in (\"mouse_move\", \"left_click_drag\"):\n            if coordinate is None:\n                raise ToolError(f\"coordinate is required for {action}\")\n            if text is not None:\n                raise ToolError(f\"text is not accepted for {action}\")\n\n            if action == \"left_click_drag\":\n                if start_coordinate is None:\n                    raise ToolError(f\"start_coordinate is required for {action}\")\n                start_x, start_y = self.validate_and_get_coordinates(start_coordinate)\n                end_x, end_y = self.validate_and_get_coordinates(coordinate)\n                command_parts = [\n                    self.xdotool,\n                    f\"mousemove --sync {start_x} {start_y} mousedown 1 mousemove --sync {end_x} {end_y} mouseup 1\",\n                ]\n                return await self.shell(\" \".join(command_parts))\n            elif action == \"mouse_move\":\n                x, y = self.validate_and_get_coordinates(coordinate)\n                command_parts = [self.xdotool, f\"mousemove --sync {x} {y}\"]\n                return await self.shell(\" \".join(command_parts))\n\n        if action in (\"key\", \"type\"):\n            if text is None:\n                raise ToolError(f\"text is required for {action}\")\n            if coordinate is not None:\n                raise ToolError(f\"coordinate is not accepted for {action}\")\n            if not isinstance(text, str):\n                raise ToolError(output=f\"{text} must be a string\")\n\n            if action == \"key\":\n                command_parts = [self.xdotool, f\"key -- {text}\"]\n                return await self.shell(\" \".join(command_parts))\n            elif action == \"type\":\n                results: list[ToolResult] = []\n                for chunk in chunks(text, TYPING_GROUP_SIZE):\n                    command_parts = [\n                        self.xdotool,\n                        f\"type --delay {TYPING_DELAY_MS} -- {shlex.quote(chunk)}\",\n                    ]\n                    results.append(\n                        await self.shell(\" \".join(command_parts), take_screenshot=False)\n                    )\n                screenshot_base64 = (await self.screenshot()).base64_image\n                return ToolResult(\n                    output=\"\".join(result.output or \"\" for result in results),\n                    error=\"\".join(result.error or \"\" for result in results),\n                    base64_image=screenshot_base64,\n                )\n\n        if action in (\n            \"left_click\",\n            \"right_click\",\n            \"double_click\",\n            \"middle_click\",\n            \"screenshot\",\n            \"cursor_position\",\n        ):\n            if text is not None:\n                raise ToolError(f\"text is not accepted for {action}\")\n            if coordinate is not None:\n                raise ToolError(f\"coordinate is not accepted for {action}\")\n\n            if action == \"screenshot\":\n                return await self.screenshot()\n            elif action == \"cursor_position\":\n                command_parts = [self.xdotool, \"getmouselocation --shell\"]\n                result = await self.shell(\n                    \" \".join(command_parts),\n                    take_screenshot=False,\n                )\n                output = result.output or \"\"\n                x, y = self.scale_coordinates(\n                    ScalingSource.COMPUTER,\n                    int(output.split(\"X=\")[1].split(\"\\n\")[0]),\n                    int(output.split(\"Y=\")[1].split(\"\\n\")[0]),\n                )\n                return result.replace(output=f\"X={x},Y={y}\")\n            else:\n                command_parts = [self.xdotool, f\"click {CLICK_BUTTONS[action]}\"]\n                return await self.shell(\" \".join(command_parts))\n\n        raise ToolError(f\"Invalid action: {action}\")\n\n    def validate_and_get_coordinates(self, coordinate: tuple[int, int] | None = None):\n        if not isinstance(coordinate, list) or len(coordinate) != 2:\n            raise ToolError(f\"{coordinate} must be a tuple of length 2\")\n        if not all(isinstance(i, int) and i >= 0 for i in coordinate):\n            raise ToolError(f\"{coordinate} must be a tuple of non-negative ints\")\n\n        return self.scale_coordinates(ScalingSource.API, coordinate[0], coordinate[1])\n\n    async def screenshot(self):\n        \"\"\"Take a screenshot of the current screen and return the base64 encoded image.\"\"\"\n        output_dir = Path(OUTPUT_DIR)\n        await asyncio.to_thread(output_dir.mkdir, parents=True, exist_ok=True)\n        path = output_dir / f\"screenshot_{uuid4().hex}.png\"\n\n        # Try gnome-screenshot first\n        if shutil.which(\"gnome-screenshot\"):\n            screenshot_cmd = f\"{self._display_prefix}gnome-screenshot -f {path} -p\"\n        else:\n            # Fall back to scrot if gnome-screenshot isn't available\n            screenshot_cmd = f\"{self._display_prefix}scrot -p {path}\"\n\n        result = await self.shell(screenshot_cmd, take_screenshot=False)\n        if self._scaling_enabled:\n            x, y = self.scale_coordinates(\n                ScalingSource.COMPUTER, self.width, self.height\n            )\n            await self.shell(\n                f\"convert {path} -resize {x}x{y}! {path}\", take_screenshot=False\n            )\n\n        if path.exists():\n            return result.replace(\n                base64_image=base64.b64encode(path.read_bytes()).decode()\n            )\n        raise ToolError(f\"Failed to take screenshot: {result.error}\")\n\n    async def shell(self, command: str, take_screenshot=True) -> ToolResult:\n        \"\"\"Run a shell command and return the output, error, and optionally a screenshot.\"\"\"\n        _, stdout, stderr = await run(command)\n        base64_image = None\n\n        if take_screenshot:\n            # delay to let things settle before taking a screenshot\n            await asyncio.sleep(self._screenshot_delay)\n            base64_image = (await self.screenshot()).base64_image\n\n        return ToolResult(output=stdout, error=stderr, base64_image=base64_image)\n\n    def scale_coordinates(self, source: ScalingSource, x: int, y: int):\n        \"\"\"Scale coordinates to a target maximum resolution.\"\"\"\n        if not self._scaling_enabled:\n            return x, y\n        ratio = self.width / self.height\n        target_dimension = None\n        for dimension in MAX_SCALING_TARGETS.values():\n            # allow some error in the aspect ratio - not ratios are exactly 16:9\n            if abs(dimension[\"width\"] / dimension[\"height\"] - ratio) < 0.02:\n                if dimension[\"width\"] < self.width:\n                    target_dimension = dimension\n                break\n        if target_dimension is None:\n            return x, y\n        # should be less than 1\n        x_scaling_factor = target_dimension[\"width\"] / self.width\n        y_scaling_factor = target_dimension[\"height\"] / self.height\n        if source == ScalingSource.API:\n            if x > self.width or y > self.height:\n                raise ToolError(f\"Coordinates {x}, {y} are out of bounds\")\n            # scale up\n            return round(x / x_scaling_factor), round(y / y_scaling_factor)\n        # scale down\n        return round(x * x_scaling_factor), round(y * y_scaling_factor)\n\n\nclass ComputerTool20241022(BaseComputerTool, BaseAnthropicTool):\n    api_type: Literal[\"computer_20241022\"] = \"computer_20241022\"\n\n    def to_params(self) -> BetaToolComputerUse20241022Param:\n        return {\"name\": self.name, \"type\": self.api_type, **self.options}\n\n\nclass ComputerTool20250124(BaseComputerTool, BaseAnthropicTool):\n    api_type: Literal[\"computer_20250124\"] = \"computer_20250124\"\n\n    def to_params(self):\n        return cast(\n            BetaToolUnionParam,\n            {\"name\": self.name, \"type\": self.api_type, **self.options},\n        )\n\n    async def __call__(\n        self,\n        *,\n        action: Action_20250124,\n        text: str | None = None,\n        coordinate: tuple[int, int] | None = None,\n        start_coordinate: tuple[int, int] | None = None,\n        scroll_direction: ScrollDirection | None = None,\n        scroll_amount: int | None = None,\n        duration: int | float | None = None,\n        key: str | None = None,\n        **kwargs,\n    ):\n        if action in (\"left_mouse_down\", \"left_mouse_up\"):\n            if coordinate is not None:\n                raise ToolError(f\"coordinate is not accepted for {action=}.\")\n            command_parts = [\n                self.xdotool,\n                f\"{'mousedown' if action == 'left_mouse_down' else 'mouseup'} 1\",\n            ]\n            return await self.shell(\" \".join(command_parts))\n        if action == \"scroll\":\n            if scroll_direction is None or scroll_direction not in get_args(\n                ScrollDirection\n            ):\n                raise ToolError(\n                    f\"{scroll_direction=} must be 'up', 'down', 'left', or 'right'\"\n                )\n            if not isinstance(scroll_amount, int) or scroll_amount < 0:\n                raise ToolError(f\"{scroll_amount=} must be a non-negative int\")\n            mouse_move_part = \"\"\n            if coordinate is not None:\n                x, y = self.validate_and_get_coordinates(coordinate)\n                mouse_move_part = f\"mousemove --sync {x} {y}\"\n            scroll_button = {\n                \"up\": 4,\n                \"down\": 5,\n                \"left\": 6,\n                \"right\": 7,\n            }[scroll_direction]\n\n            command_parts = [self.xdotool, mouse_move_part]\n            if text:\n                command_parts.append(f\"keydown {text}\")\n            command_parts.append(f\"click --repeat {scroll_amount} {scroll_button}\")\n            if text:\n                command_parts.append(f\"keyup {text}\")\n\n            return await self.shell(\" \".join(command_parts))\n\n        if action in (\"hold_key\", \"wait\"):\n            if duration is None or not isinstance(duration, (int, float)):\n                raise ToolError(f\"{duration=} must be a number\")\n            if duration < 0:\n                raise ToolError(f\"{duration=} must be non-negative\")\n            if duration > 100:\n                raise ToolError(f\"{duration=} is too long.\")\n\n            if action == \"hold_key\":\n                if text is None:\n                    raise ToolError(f\"text is required for {action}\")\n                escaped_keys = shlex.quote(text)\n                command_parts = [\n                    self.xdotool,\n                    f\"keydown {escaped_keys}\",\n                    f\"sleep {duration}\",\n                    f\"keyup {escaped_keys}\",\n                ]\n                return await self.shell(\" \".join(command_parts))\n\n            if action == \"wait\":\n                await asyncio.sleep(duration)\n                return await self.screenshot()\n\n        if action in (\n            \"left_click\",\n            \"right_click\",\n            \"double_click\",\n            \"triple_click\",\n            \"middle_click\",\n        ):\n            if text is not None:\n                raise ToolError(f\"text is not accepted for {action}\")\n            mouse_move_part = \"\"\n            if coordinate is not None:\n                x, y = self.validate_and_get_coordinates(coordinate)\n                mouse_move_part = f\"mousemove --sync {x} {y}\"\n\n            command_parts = [self.xdotool, mouse_move_part]\n            if key:\n                command_parts.append(f\"keydown {key}\")\n            command_parts.append(f\"click {CLICK_BUTTONS[action]}\")\n            if key:\n                command_parts.append(f\"keyup {key}\")\n\n            return await self.shell(\" \".join(command_parts))\n\n        return await super().__call__(\n            action=action,\n            text=text,\n            coordinate=coordinate,\n            start_coordinate=start_coordinate,\n            key=key,\n            **kwargs,\n        )\n\n\nclass ComputerTool20251124(ComputerTool20250124):\n    api_type: Literal[\"computer_20251124\"] = \"computer_20251124\"  # pyright: ignore[reportIncompatibleVariableOverride]\n\n    @property\n    def options(self) -> ComputerToolOptions:  # pyright: ignore[reportIncompatibleMethodOverride]\n        return {**super().options, \"enable_zoom\": True}  # pyright: ignore[reportReturnType]\n\n    async def __call__(\n        self,\n        *,\n        action: Action_20251124,\n        text: str | None = None,\n        coordinate: tuple[int, int] | None = None,\n        scroll_direction: ScrollDirection | None = None,\n        scroll_amount: int | None = None,\n        duration: int | float | None = None,\n        key: str | None = None,\n        region: tuple[int, int, int, int] | None = None,\n        **kwargs,\n    ):\n        if action == \"zoom\":\n            if (\n                region is None\n                or not isinstance(region, (list, tuple))\n                or len(region) != 4\n            ):\n                raise ToolError(\n                    f\"{region=} must be a tuple of 4 coordinates (x0, y0, x1, y1)\"\n                )\n            if not all(isinstance(c, int) and c >= 0 for c in region):\n                raise ToolError(f\"{region=} must contain non-negative integers\")\n\n            x0, y0, x1, y1 = region\n            # Scale coordinates from API space to screen space\n            x0, y0 = self.scale_coordinates(ScalingSource.API, x0, y0)\n            x1, y1 = self.scale_coordinates(ScalingSource.API, x1, y1)\n\n            # Take a screenshot and crop to the specified region\n            screenshot_result = await self.screenshot()\n            if not screenshot_result.base64_image:\n                raise ToolError(\"Failed to take screenshot for zoom\")\n\n            # Crop the image using ImageMagick convert\n            output_dir = Path(OUTPUT_DIR)\n            temp_path = output_dir / f\"screenshot_{uuid4().hex}.png\"\n            cropped_path = output_dir / f\"zoomed_{uuid4().hex}.png\"\n\n            # Write the screenshot to a temp file\n            temp_path.write_bytes(base64.b64decode(screenshot_result.base64_image))\n\n            # Crop using ImageMagick: convert input -crop WxH+X+Y output\n            width = x1 - x0\n            height = y1 - y0\n            crop_cmd = f\"convert {temp_path} -crop {width}x{height}+{x0}+{y0} +repage {cropped_path}\"\n            await run(crop_cmd)\n\n            if cropped_path.exists():\n                cropped_base64 = base64.b64encode(cropped_path.read_bytes()).decode()\n                temp_path.unlink(missing_ok=True)\n                cropped_path.unlink(missing_ok=True)\n                return ToolResult(base64_image=cropped_base64)\n\n            raise ToolError(\"Failed to crop screenshot for zoom\")\n\n        return await super().__call__(\n            action=action,\n            text=text,\n            coordinate=coordinate,\n            scroll_direction=scroll_direction,\n            scroll_amount=scroll_amount,\n            duration=duration,\n            key=key,\n            **kwargs,\n        )\n"
  },
  {
    "path": "computer-use-demo/computer_use_demo/tools/edit.py",
    "content": "import asyncio\nfrom collections import defaultdict\nfrom pathlib import Path\nfrom typing import Any, Literal, get_args\n\nfrom .base import BaseAnthropicTool, CLIResult, ToolError, ToolResult\nfrom .run import maybe_truncate, run\n\nCommand = Literal[\n    \"view\",\n    \"create\",\n    \"str_replace\",\n    \"insert\",\n]\nSNIPPET_LINES: int = 4\n\n\nclass EditTool20250728(BaseAnthropicTool):\n    \"\"\"\n    A filesystem editor tool that allows the agent to view, create, and edit files.\n    The tool parameters are defined by Anthropic and are not editable.\n\n    This implements the text_editor_20250728 tool version, which:\n    - Uses `insert_text` (not `new_str`) for the insert command\n    - Uses `str_replace_based_edit_tool` as the tool name\n    - Does not support `undo_edit`\n    \"\"\"\n\n    api_type: Literal[\"text_editor_20250728\"] = \"text_editor_20250728\"\n    name: Literal[\"str_replace_based_edit_tool\"] = \"str_replace_based_edit_tool\"\n\n    _file_history: dict[Path, list[str]]\n\n    def __init__(self):\n        self._file_history = defaultdict(list)\n        super().__init__()\n\n    def to_params(self) -> Any:\n        return {\n            \"name\": self.name,\n            \"type\": self.api_type,\n        }\n\n    async def __call__(\n        self,\n        *,\n        command: Command,\n        path: str,\n        file_text: str | None = None,\n        view_range: list[int] | None = None,\n        old_str: str | None = None,\n        new_str: str | None = None,\n        insert_line: int | None = None,\n        insert_text: str | None = None,\n        **kwargs,\n    ):\n        _path = Path(path)\n        self.validate_path(command, _path)\n        if command == \"view\":\n            return await self.view(_path, view_range)\n        elif command == \"create\":\n            if file_text is None:\n                raise ToolError(\"Parameter `file_text` is required for command: create\")\n            self.write_file(_path, file_text)\n            self._file_history[_path].append(file_text)\n            return ToolResult(output=f\"File created successfully at: {_path}\")\n        elif command == \"str_replace\":\n            if old_str is None:\n                raise ToolError(\n                    \"Parameter `old_str` is required for command: str_replace\"\n                )\n            return self.str_replace(_path, old_str, new_str)\n        elif command == \"insert\":\n            if insert_line is None:\n                raise ToolError(\n                    \"Parameter `insert_line` is required for command: insert\"\n                )\n            if insert_text is None:\n                raise ToolError(\n                    \"Parameter `insert_text` is required for command: insert\"\n                )\n            return self.insert(_path, insert_line, insert_text)\n        raise ToolError(\n            f\"Unrecognized command {command}. The allowed commands for the {self.name} tool are: {', '.join(get_args(Command))}\"\n        )\n\n    def validate_path(self, command: str, path: Path):\n        \"\"\"\n        Check that the path/command combination is valid.\n        \"\"\"\n        # Check if its an absolute path\n        if not path.is_absolute():\n            suggested_path = Path(\"\") / path\n            raise ToolError(\n                f\"The path {path} is not an absolute path, it should start with `/`. Maybe you meant {suggested_path}?\"\n            )\n        # Check if path exists\n        if not path.exists() and command != \"create\":\n            raise ToolError(\n                f\"The path {path} does not exist. Please provide a valid path.\"\n            )\n        if path.exists() and command == \"create\":\n            raise ToolError(\n                f\"File already exists at: {path}. Cannot overwrite files using command `create`.\"\n            )\n        # Check if the path points to a directory\n        if path.is_dir():\n            if command != \"view\":\n                raise ToolError(\n                    f\"The path {path} is a directory and only the `view` command can be used on directories\"\n                )\n\n    async def view(self, path: Path, view_range: list[int] | None = None):\n        \"\"\"Implement the view command\"\"\"\n        if await asyncio.to_thread(path.is_dir):\n            if view_range:\n                raise ToolError(\n                    \"The `view_range` parameter is not allowed when `path` points to a directory.\"\n                )\n\n            _, stdout, stderr = await run(\n                rf\"find {path} -maxdepth 2 -not -path '*/\\.*'\"\n            )\n            if not stderr:\n                stdout = f\"Here's the files and directories up to 2 levels deep in {path}, excluding hidden items:\\n{stdout}\\n\"\n            return CLIResult(output=stdout, error=stderr)\n\n        file_content = self.read_file(path)\n        init_line = 1\n        if view_range:\n            if len(view_range) != 2 or not all(isinstance(i, int) for i in view_range):\n                raise ToolError(\n                    \"Invalid `view_range`. It should be a list of two integers.\"\n                )\n            file_lines = file_content.split(\"\\n\")\n            n_lines_file = len(file_lines)\n            init_line, final_line = view_range\n            if init_line < 1 or init_line > n_lines_file:\n                raise ToolError(\n                    f\"Invalid `view_range`: {view_range}. Its first element `{init_line}` should be within the range of lines of the file: {[1, n_lines_file]}\"\n                )\n            if final_line > n_lines_file:\n                raise ToolError(\n                    f\"Invalid `view_range`: {view_range}. Its second element `{final_line}` should be smaller than the number of lines in the file: `{n_lines_file}`\"\n                )\n            if final_line != -1 and final_line < init_line:\n                raise ToolError(\n                    f\"Invalid `view_range`: {view_range}. Its second element `{final_line}` should be larger or equal than its first `{init_line}`\"\n                )\n\n            if final_line == -1:\n                file_content = \"\\n\".join(file_lines[init_line - 1 :])\n            else:\n                file_content = \"\\n\".join(file_lines[init_line - 1 : final_line])\n\n        return CLIResult(\n            output=self._make_output(file_content, str(path), init_line=init_line)\n        )\n\n    def str_replace(self, path: Path, old_str: str, new_str: str | None):\n        \"\"\"Implement the str_replace command, which replaces old_str with new_str in the file content\"\"\"\n        # Read the file content\n        file_content = self.read_file(path).expandtabs()\n        old_str = old_str.expandtabs()\n        new_str = new_str.expandtabs() if new_str is not None else \"\"\n\n        # Check if old_str is unique in the file\n        occurrences = file_content.count(old_str)\n        if occurrences == 0:\n            raise ToolError(\n                f\"No replacement was performed, old_str `{old_str}` did not appear verbatim in {path}.\"\n            )\n        elif occurrences > 1:\n            file_content_lines = file_content.split(\"\\n\")\n            lines = [\n                idx + 1\n                for idx, line in enumerate(file_content_lines)\n                if old_str in line\n            ]\n            raise ToolError(\n                f\"No replacement was performed. Multiple occurrences of old_str `{old_str}` in lines {lines}. Please ensure it is unique\"\n            )\n\n        # Replace old_str with new_str\n        new_file_content = file_content.replace(old_str, new_str)\n\n        # Write the new content to the file\n        self.write_file(path, new_file_content)\n\n        # Save the content to history\n        self._file_history[path].append(file_content)\n\n        # Create a snippet of the edited section\n        replacement_line = file_content.split(old_str)[0].count(\"\\n\")\n        start_line = max(0, replacement_line - SNIPPET_LINES)\n        end_line = replacement_line + SNIPPET_LINES + new_str.count(\"\\n\")\n        snippet = \"\\n\".join(new_file_content.split(\"\\n\")[start_line : end_line + 1])\n\n        # Prepare the success message\n        success_msg = f\"The file {path} has been edited. \"\n        success_msg += self._make_output(\n            snippet, f\"a snippet of {path}\", start_line + 1\n        )\n        success_msg += \"Review the changes and make sure they are as expected. Edit the file again if necessary.\"\n\n        return CLIResult(output=success_msg)\n\n    def insert(self, path: Path, insert_line: int, new_str: str):\n        \"\"\"Implement the insert command, which inserts new_str at the specified line in the file content.\"\"\"\n        file_text = self.read_file(path).expandtabs()\n        new_str = new_str.expandtabs()\n        file_text_lines = file_text.split(\"\\n\")\n        n_lines_file = len(file_text_lines)\n\n        if insert_line < 0 or insert_line > n_lines_file:\n            raise ToolError(\n                f\"Invalid `insert_line` parameter: {insert_line}. It should be within the range of lines of the file: {[0, n_lines_file]}\"\n            )\n\n        new_str_lines = new_str.split(\"\\n\")\n        new_file_text_lines = (\n            file_text_lines[:insert_line]\n            + new_str_lines\n            + file_text_lines[insert_line:]\n        )\n        snippet_lines = (\n            file_text_lines[max(0, insert_line - SNIPPET_LINES) : insert_line]\n            + new_str_lines\n            + file_text_lines[insert_line : insert_line + SNIPPET_LINES]\n        )\n\n        new_file_text = \"\\n\".join(new_file_text_lines)\n        snippet = \"\\n\".join(snippet_lines)\n\n        self.write_file(path, new_file_text)\n        self._file_history[path].append(file_text)\n\n        success_msg = f\"The file {path} has been edited. \"\n        success_msg += self._make_output(\n            snippet,\n            \"a snippet of the edited file\",\n            max(1, insert_line - SNIPPET_LINES + 1),\n        )\n        success_msg += \"Review the changes and make sure they are as expected (correct indentation, no duplicate lines, etc). Edit the file again if necessary.\"\n        return CLIResult(output=success_msg)\n\n    def read_file(self, path: Path):\n        \"\"\"Read the content of a file from a given path; raise a ToolError if an error occurs.\"\"\"\n        try:\n            return path.read_text()\n        except Exception as e:\n            raise ToolError(f\"Ran into {e} while trying to read {path}\") from None\n\n    def write_file(self, path: Path, file: str):\n        \"\"\"Write the content of a file to a given path; raise a ToolError if an error occurs.\"\"\"\n        try:\n            path.write_text(file)\n        except Exception as e:\n            raise ToolError(f\"Ran into {e} while trying to write to {path}\") from None\n\n    def _make_output(\n        self,\n        file_content: str,\n        file_descriptor: str,\n        init_line: int = 1,\n        expand_tabs: bool = True,\n    ):\n        \"\"\"Generate output for the CLI based on the content of a file.\"\"\"\n        file_content = maybe_truncate(file_content)\n        if expand_tabs:\n            file_content = file_content.expandtabs()\n        file_content = \"\\n\".join(\n            [\n                f\"{i + init_line:6}\\t{line}\"\n                for i, line in enumerate(file_content.split(\"\\n\"))\n            ]\n        )\n        return (\n            f\"Here's the result of running `cat -n` on {file_descriptor}:\\n\"\n            + file_content\n            + \"\\n\"\n        )\n"
  },
  {
    "path": "computer-use-demo/computer_use_demo/tools/groups.py",
    "content": "from dataclasses import dataclass\nfrom typing import Literal\n\nfrom .base import BaseAnthropicTool\nfrom .bash import BashTool20241022, BashTool20250124\nfrom .computer import ComputerTool20241022, ComputerTool20250124, ComputerTool20251124\nfrom .edit import EditTool20250728\n\nToolVersion = Literal[\n    \"computer_use_20250124\",\n    \"computer_use_20241022\",\n    \"computer_use_20250429\",\n    \"computer_use_20251124\",\n]\nBetaFlag = Literal[\n    \"computer-use-2024-10-22\",\n    \"computer-use-2025-01-24\",\n    \"computer-use-2025-04-29\",\n    \"computer-use-2025-11-24\",\n]\n\n\n@dataclass(frozen=True, kw_only=True)\nclass ToolGroup:\n    version: ToolVersion\n    tools: list[type[BaseAnthropicTool]]\n    beta_flag: BetaFlag | None = None\n\n\nTOOL_GROUPS: list[ToolGroup] = [\n    ToolGroup(\n        version=\"computer_use_20241022\",\n        tools=[ComputerTool20241022, EditTool20250728, BashTool20241022],\n        beta_flag=\"computer-use-2024-10-22\",\n    ),\n    ToolGroup(\n        version=\"computer_use_20250124\",\n        tools=[ComputerTool20250124, EditTool20250728, BashTool20250124],\n        beta_flag=\"computer-use-2025-01-24\",\n    ),\n    ToolGroup(\n        version=\"computer_use_20250429\",\n        tools=[ComputerTool20250124, EditTool20250728, BashTool20250124],\n        beta_flag=\"computer-use-2025-01-24\",\n    ),\n    ToolGroup(\n        version=\"computer_use_20251124\",\n        tools=[ComputerTool20251124, EditTool20250728, BashTool20250124],\n        beta_flag=\"computer-use-2025-11-24\",\n    ),\n]\n\nTOOL_GROUPS_BY_VERSION = {tool_group.version: tool_group for tool_group in TOOL_GROUPS}\n"
  },
  {
    "path": "computer-use-demo/computer_use_demo/tools/run.py",
    "content": "\"\"\"Utility to run shell commands asynchronously with a timeout.\"\"\"\n\nimport asyncio\n\nTRUNCATED_MESSAGE: str = \"<response clipped><NOTE>To save on context only part of this file has been shown to you. You should retry this tool after you have searched inside the file with `grep -n` in order to find the line numbers of what you are looking for.</NOTE>\"\nMAX_RESPONSE_LEN: int = 16000\n\n\ndef maybe_truncate(content: str, truncate_after: int | None = MAX_RESPONSE_LEN):\n    \"\"\"Truncate content and append a notice if content exceeds the specified length.\"\"\"\n    return (\n        content\n        if not truncate_after or len(content) <= truncate_after\n        else content[:truncate_after] + TRUNCATED_MESSAGE\n    )\n\n\nasync def run(\n    cmd: str,\n    timeout: float | None = 120.0,  # seconds\n    truncate_after: int | None = MAX_RESPONSE_LEN,\n):\n    \"\"\"Run a shell command asynchronously with a timeout.\"\"\"\n    process = await asyncio.create_subprocess_shell(\n        cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE\n    )\n\n    try:\n        stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout)\n        return (\n            process.returncode or 0,\n            maybe_truncate(stdout.decode(), truncate_after=truncate_after),\n            maybe_truncate(stderr.decode(), truncate_after=truncate_after),\n        )\n    except asyncio.TimeoutError as exc:\n        try:\n            process.kill()\n        except ProcessLookupError:\n            pass\n        raise TimeoutError(\n            f\"Command '{cmd}' timed out after {timeout} seconds\"\n        ) from exc\n"
  },
  {
    "path": "computer-use-demo/dev-requirements.txt",
    "content": "-r computer_use_demo/requirements.txt\nruff==0.6.7\npre-commit==3.8.0\npytest==8.3.3\npytest-asyncio==0.23.6\n"
  },
  {
    "path": "computer-use-demo/image/.config/tint2/applications/firefox-custom.desktop",
    "content": "[Desktop Entry]\nName=Firefox Custom\nComment=Open Firefox with custom URL\nExec=firefox-esr -new-window\nIcon=firefox-esr\nTerminal=false\nType=Application\nCategories=Network;WebBrowser;\n"
  },
  {
    "path": "computer-use-demo/image/.config/tint2/applications/gedit.desktop",
    "content": "[Desktop Entry]\nName=Gedit\nComment=Open gedit\nExec=gedit\nIcon=text-editor-symbolic\nTerminal=false\nType=Application\nCategories=TextEditor;\n"
  },
  {
    "path": "computer-use-demo/image/.config/tint2/applications/terminal.desktop",
    "content": "[Desktop Entry]\nName=Terminal\nComment=Open Terminal\nExec=xterm\nIcon=utilities-terminal\nTerminal=false\nType=Application\nCategories=System;TerminalEmulator;\n"
  },
  {
    "path": "computer-use-demo/image/.config/tint2/tint2rc",
    "content": "#-------------------------------------\n# Panel\npanel_items = TL\npanel_size = 100% 60\npanel_margin = 0 0\npanel_padding = 2 0 2\npanel_background_id = 1\nwm_menu = 0\npanel_dock = 0\npanel_position = bottom center horizontal\npanel_layer = top\npanel_monitor = all\npanel_shrink = 0\nautohide = 0\nautohide_show_timeout = 0\nautohide_hide_timeout = 0.5\nautohide_height = 2\nstrut_policy = follow_size\npanel_window_name = tint2\ndisable_transparency = 1\nmouse_effects = 1\nfont_shadow = 0\nmouse_hover_icon_asb = 100 0 10\nmouse_pressed_icon_asb = 100 0 0\nscale_relative_to_dpi = 0\nscale_relative_to_screen_height = 0\n\n#-------------------------------------\n# Taskbar\ntaskbar_mode = single_desktop\ntaskbar_hide_if_empty = 0\ntaskbar_padding = 0 0 2\ntaskbar_background_id = 0\ntaskbar_active_background_id = 0\ntaskbar_name = 1\ntaskbar_hide_inactive_tasks = 0\ntaskbar_hide_different_monitor = 0\ntaskbar_hide_different_desktop = 0\ntaskbar_always_show_all_desktop_tasks = 0\ntaskbar_name_padding = 4 2\ntaskbar_name_background_id = 0\ntaskbar_name_active_background_id = 0\ntaskbar_name_font_color = #e3e3e3 100\ntaskbar_name_active_font_color = #ffffff 100\ntaskbar_distribute_size = 0\ntaskbar_sort_order = none\ntask_align = left\n\n#-------------------------------------\n# Launcher\nlauncher_padding = 4 8 4\nlauncher_background_id = 0\nlauncher_icon_background_id = 0\nlauncher_icon_size = 48\nlauncher_icon_asb = 100 0 0\nlauncher_icon_theme_override = 0\nstartup_notifications = 1\nlauncher_tooltip = 1\n\n#-------------------------------------\n# Launcher icon\nlauncher_item_app = /usr/share/applications/libreoffice-calc.desktop\nlauncher_item_app = /home/computeruse/.config/tint2/applications/terminal.desktop\nlauncher_item_app = /home/computeruse/.config/tint2/applications/firefox-custom.desktop\nlauncher_item_app = /usr/share/applications/xpaint.desktop\nlauncher_item_app = /usr/share/applications/xpdf.desktop\nlauncher_item_app = /home/computeruse/.config/tint2/applications/gedit.desktop\nlauncher_item_app = /usr/share/applications/galculator.desktop\n\n#-------------------------------------\n# Background definitions\n# ID 1\nrounded = 0\nborder_width = 0\nbackground_color = #000000 60\nborder_color = #000000 30\n\n# ID 2\nrounded = 4\nborder_width = 1\nbackground_color = #777777 20\nborder_color = #777777 30\n\n# ID 3\nrounded = 4\nborder_width = 1\nbackground_color = #777777 20\nborder_color = #ffffff 40\n\n# ID 4\nrounded = 4\nborder_width = 1\nbackground_color = #aa4400 100\nborder_color = #aa7733 100\n\n# ID 5\nrounded = 4\nborder_width = 1\nbackground_color = #aaaa00 100\nborder_color = #aaaa00 100\n"
  },
  {
    "path": "computer-use-demo/image/.streamlit/config.toml",
    "content": "[server]\nfileWatcherType = \"auto\"\nrunOnSave = true\n\n[browser]\ngatherUsageStats = false\n"
  },
  {
    "path": "computer-use-demo/image/entrypoint.sh",
    "content": "#!/bin/bash\nset -e\n\n./start_all.sh\n./novnc_startup.sh\n\npython http_server.py > /tmp/server_logs.txt 2>&1 &\n\nSTREAMLIT_SERVER_PORT=8501 python -m streamlit run computer_use_demo/streamlit.py > /tmp/streamlit_stdout.log &\n\necho \"✨ Computer Use Demo is ready!\"\necho \"➡️  Open http://localhost:8080 in your browser to begin\"\n\n# Keep the container running\ntail -f /dev/null\n"
  },
  {
    "path": "computer-use-demo/image/http_server.py",
    "content": "import os\nimport socket\nfrom http.server import HTTPServer, SimpleHTTPRequestHandler\n\n\nclass HTTPServerV6(HTTPServer):\n    address_family = socket.AF_INET6\n\n\ndef run_server():\n    os.chdir(os.path.dirname(__file__) + \"/static_content\")\n    server_address = (\"::\", 8080)\n    httpd = HTTPServerV6(server_address, SimpleHTTPRequestHandler)\n    print(\"Starting HTTP server on port 8080...\")  # noqa: T201\n    httpd.serve_forever()\n\n\nif __name__ == \"__main__\":\n    run_server()\n"
  },
  {
    "path": "computer-use-demo/image/index.html",
    "content": "<!doctype html>\n<html>\n    <head>\n        <title>Computer Use Demo</title>\n        <meta name=\"permissions-policy\" content=\"fullscreen=*\" />\n        <style>\n            body {\n                margin: 0;\n                padding: 0;\n                overflow: hidden;\n            }\n            .container {\n                display: flex;\n                height: 100vh;\n                width: 100vw;\n            }\n            .left {\n                flex: 1;\n                border: none;\n                height: 100vh;\n            }\n            .right {\n                flex: 2;\n                border: none;\n                height: 100vh;\n            }\n        </style>\n    </head>\n    <body>\n        <div class=\"container\">\n            <iframe\n                src=\"http://localhost:8501\"\n                class=\"left\"\n                allow=\"fullscreen\"\n            ></iframe>\n            <iframe\n                src=\"http://localhost:6080/vnc.html?view_only=1&autoconnect=1&resize=scale\"\n                class=\"right\"\n                allow=\"fullscreen\"\n            ></iframe>\n        </div>\n    </body>\n</html>\n"
  },
  {
    "path": "computer-use-demo/image/mutter_startup.sh",
    "content": "echo \"starting mutter\"\nXDG_SESSION_TYPE=x11 mutter --replace --sm-disable 2>/tmp/mutter_stderr.log &\n\n# Wait for tint2 window properties to appear\ntimeout=30\nwhile [ $timeout -gt 0 ]; do\n    if xdotool search --class \"mutter\" >/dev/null 2>&1; then\n        break\n    fi\n    sleep 1\n    ((timeout--))\ndone\n\nif [ $timeout -eq 0 ]; then\n    echo \"mutter stderr output:\" >&2\n    cat /tmp/mutter_stderr.log >&2\n    exit 1\nfi\n\nrm /tmp/mutter_stderr.log\n"
  },
  {
    "path": "computer-use-demo/image/novnc_startup.sh",
    "content": "#!/bin/bash\necho \"starting noVNC\"\n\n# Start noVNC with explicit websocket settings\n/opt/noVNC/utils/novnc_proxy \\\n    --vnc localhost:5900 \\\n    --listen 6080 \\\n    --web /opt/noVNC \\\n    > /tmp/novnc.log 2>&1 &\n\n# Wait for noVNC to start\ntimeout=10\nwhile [ $timeout -gt 0 ]; do\n    if netstat -tuln | grep -q \":6080 \"; then\n        break\n    fi\n    sleep 1\n    ((timeout--))\ndone\n\necho \"noVNC started successfully\"\n"
  },
  {
    "path": "computer-use-demo/image/start_all.sh",
    "content": "#!/bin/bash\n\nset -e\n\nexport DISPLAY=:${DISPLAY_NUM}\n./xvfb_startup.sh\n./tint2_startup.sh\n./mutter_startup.sh\n./x11vnc_startup.sh\n"
  },
  {
    "path": "computer-use-demo/image/static_content/index.html",
    "content": "<!doctype html>\n<html>\n    <head>\n        <title>Computer Use Demo</title>\n        <meta name=\"permissions-policy\" content=\"fullscreen=*\" />\n        <style>\n            body {\n                margin: 0;\n                padding: 0;\n                overflow: hidden;\n            }\n            .container {\n                display: flex;\n                height: 100vh;\n                width: 100vw;\n            }\n            .left {\n                flex: 1;\n                border: none;\n                height: 100vh;\n            }\n            .right {\n                flex: 2;\n                border: none;\n                height: 100vh;\n            }\n        </style>\n    </head>\n    <body>\n        <div class=\"container\">\n            <iframe\n                src=\"http://localhost:8501\"\n                class=\"left\"\n                allow=\"fullscreen\"\n            ></iframe>\n            <iframe\n                id=\"vnc\"\n                src=\"http://127.0.0.1:6080/vnc.html?&resize=scale&autoconnect=1&view_only=1&reconnect=1&reconnect_delay=2000\"\n                class=\"right\"\n                allow=\"fullscreen\"\n            ></iframe>\n            <button\n                id=\"toggleViewOnly\"\n                style=\"position: absolute; top: 10px; right: 10px; z-index: 1000\"\n            >\n                Toggle Screen Control (Off)\n            </button>\n            <script>\n                document\n                    .getElementById(\"toggleViewOnly\")\n                    .addEventListener(\"click\", function () {\n                        var vncIframe = document.getElementById(\"vnc\");\n                        var button = document.getElementById(\"toggleViewOnly\");\n                        var currentSrc = vncIframe.src;\n                        if (currentSrc.includes(\"view_only=1\")) {\n                            vncIframe.src = currentSrc.replace(\n                                \"view_only=1\",\n                                \"view_only=0\",\n                            );\n                            button.innerText = \"Toggle Screen Control (On)\";\n                        } else {\n                            vncIframe.src = currentSrc.replace(\n                                \"view_only=0\",\n                                \"view_only=1\",\n                            );\n                            button.innerText = \"Toggle Screen Control (Off)\";\n                        }\n                    });\n            </script>\n        </div>\n    </body>\n</html>\n"
  },
  {
    "path": "computer-use-demo/image/tint2_startup.sh",
    "content": "#!/bin/bash\necho \"starting tint2 on display :$DISPLAY_NUM ...\"\n\n# Start tint2 and capture its stderr\ntint2 -c $HOME/.config/tint2/tint2rc 2>/tmp/tint2_stderr.log &\n\n# Wait for tint2 window properties to appear\ntimeout=30\nwhile [ $timeout -gt 0 ]; do\n    if xdotool search --class \"tint2\" >/dev/null 2>&1; then\n        break\n    fi\n    sleep 1\n    ((timeout--))\ndone\n\nif [ $timeout -eq 0 ]; then\n    echo \"tint2 stderr output:\" >&2\n    cat /tmp/tint2_stderr.log >&2\n    exit 1\nfi\n\n# Remove the temporary stderr log file\nrm /tmp/tint2_stderr.log\n"
  },
  {
    "path": "computer-use-demo/image/x11vnc_startup.sh",
    "content": "#!/bin/bash\necho \"starting vnc\"\n\n(x11vnc -display $DISPLAY \\\n    -forever \\\n    -shared \\\n    -wait 50 \\\n    -rfbport 5900 \\\n    -nopw \\\n    2>/tmp/x11vnc_stderr.log) &\n\nx11vnc_pid=$!\n\n# Wait for x11vnc to start\ntimeout=10\nwhile [ $timeout -gt 0 ]; do\n    if netstat -tuln | grep -q \":5900 \"; then\n        break\n    fi\n    sleep 1\n    ((timeout--))\ndone\n\nif [ $timeout -eq 0 ]; then\n    echo \"x11vnc failed to start, stderr output:\" >&2\n    cat /tmp/x11vnc_stderr.log >&2\n    exit 1\nfi\n\n: > /tmp/x11vnc_stderr.log\n\n# Monitor x11vnc process in the background\n(\n    while true; do\n        if ! kill -0 $x11vnc_pid 2>/dev/null; then\n            echo \"x11vnc process crashed, restarting...\" >&2\n            if [ -f /tmp/x11vnc_stderr.log ]; then\n                echo \"x11vnc stderr output:\" >&2\n                cat /tmp/x11vnc_stderr.log >&2\n                rm /tmp/x11vnc_stderr.log\n            fi\n            exec \"$0\"\n        fi\n        sleep 5\n    done\n) &\n"
  },
  {
    "path": "computer-use-demo/image/xvfb_startup.sh",
    "content": "#!/bin/bash\nset -e  # Exit on error\n\nDPI=96\nRES_AND_DEPTH=${WIDTH}x${HEIGHT}x24\n\n# Function to check if Xvfb is already running\ncheck_xvfb_running() {\n    if [ -e /tmp/.X${DISPLAY_NUM}-lock ]; then\n        return 0  # Xvfb is already running\n    else\n        return 1  # Xvfb is not running\n    fi\n}\n\n# Function to check if Xvfb is ready\nwait_for_xvfb() {\n    local timeout=10\n    local start_time=$(date +%s)\n    while ! xdpyinfo >/dev/null 2>&1; do\n        if [ $(($(date +%s) - start_time)) -gt $timeout ]; then\n            echo \"Xvfb failed to start within $timeout seconds\" >&2\n            return 1\n        fi\n        sleep 0.1\n    done\n    return 0\n}\n\n# Check if Xvfb is already running\nif check_xvfb_running; then\n    echo \"Xvfb is already running on display ${DISPLAY}\"\n    exit 0\nfi\n\n# Start Xvfb\nXvfb $DISPLAY -ac -screen 0 $RES_AND_DEPTH -retro -dpi $DPI -nolisten tcp -nolisten unix &\nXVFB_PID=$!\n\n# Wait for Xvfb to start\nif wait_for_xvfb; then\n    echo \"Xvfb started successfully on display ${DISPLAY}\"\n    echo \"Xvfb PID: $XVFB_PID\"\nelse\n    echo \"Xvfb failed to start\"\n    kill $XVFB_PID\n    exit 1\nfi\n"
  },
  {
    "path": "computer-use-demo/pyproject.toml",
    "content": "[tool.pyright]\nvenvPath = \".\"\nvenv = \".venv\"\nuseLibraryCodeForTypes = false\n\n[tool.pytest.ini_options]\npythonpath = \".\"\nasyncio_mode = \"auto\"\n"
  },
  {
    "path": "computer-use-demo/ruff.toml",
    "content": "extend-exclude = [\".venv\"]\n\n[format]\ndocstring-code-format = true\n\n[lint]\nselect = [\n    \"A\",\n    \"ASYNC\",\n    \"B\",\n    \"E\",\n    \"F\",\n    \"I\",\n    \"PIE\",\n    \"RUF200\",\n    \"T20\",\n    \"UP\",\n    \"W\",\n]\n\nignore = [\"E501\", \"ASYNC230\"]\n\n[lint.isort]\ncombine-as-imports = true\n"
  },
  {
    "path": "computer-use-demo/setup.sh",
    "content": "#!/bin/bash\nPYTHON_MINOR_VERSION=$(python3 --version | awk -F. '{print $2}')\n\nif [ \"$PYTHON_MINOR_VERSION\" -gt 12 ]; then\n    echo \"Python version 3.$PYTHON_MINOR_VERSION detected. Python 3.12 or lower is required for setup to complete.\"\n    echo \"If you have multiple versions of Python installed, you can set the correct one by adjusting setup.sh to use a specific version, for example:\"\n    echo \"'python3 -m venv .venv' -> 'python3.12 -m venv .venv'\"\n    exit 1\nfi\n\nif ! command -v cargo &> /dev/null; then\n    echo \"Cargo (the package manager for Rust) is not present.  This is required for one of this module's dependencies.\"\n    echo \"See https://www.rust-lang.org/tools/install for installation instructions.\"\n    exit 1\nfi\n\npython3 -m venv .venv\nsource .venv/bin/activate\npip install --upgrade pip\npip install -r dev-requirements.txt\npre-commit install\n"
  },
  {
    "path": "computer-use-demo/tests/conftest.py",
    "content": "import os\nfrom unittest import mock\n\nimport pytest\n\n\n@pytest.fixture(autouse=True)\ndef mock_screen_dimensions():\n    with mock.patch.dict(\n        os.environ, {\"HEIGHT\": \"768\", \"WIDTH\": \"1024\", \"DISPLAY_NUM\": \"1\"}\n    ):\n        yield\n"
  },
  {
    "path": "computer-use-demo/tests/loop_test.py",
    "content": "from unittest import mock\n\nfrom anthropic.types import TextBlock, ToolUseBlock\nfrom anthropic.types.beta import BetaMessage, BetaMessageParam, BetaTextBlockParam\n\nfrom computer_use_demo.loop import APIProvider, sampling_loop\n\n\nasync def test_loop():\n    client = mock.Mock()\n    client.beta.messages.with_raw_response.create.return_value = mock.Mock()\n    client.beta.messages.with_raw_response.create.return_value.parse.side_effect = [\n        mock.Mock(\n            spec=BetaMessage,\n            content=[\n                TextBlock(type=\"text\", text=\"Hello\"),\n                ToolUseBlock(\n                    type=\"tool_use\", id=\"1\", name=\"computer\", input={\"action\": \"test\"}\n                ),\n            ],\n        ),\n        mock.Mock(spec=BetaMessage, content=[TextBlock(type=\"text\", text=\"Done!\")]),\n    ]\n\n    tool_collection = mock.AsyncMock()\n    tool_collection.run.return_value = mock.Mock(\n        output=\"Tool output\", error=None, base64_image=None\n    )\n\n    output_callback = mock.Mock()\n    tool_output_callback = mock.Mock()\n    api_response_callback = mock.Mock()\n\n    with (\n        mock.patch(\"computer_use_demo.loop.Anthropic\", return_value=client),\n        mock.patch(\n            \"computer_use_demo.loop.ToolCollection\", return_value=tool_collection\n        ),\n    ):\n        messages: list[BetaMessageParam] = [{\"role\": \"user\", \"content\": \"Test message\"}]\n        result = await sampling_loop(\n            model=\"test-model\",\n            provider=APIProvider.ANTHROPIC,\n            system_prompt_suffix=\"\",\n            messages=messages,\n            output_callback=output_callback,\n            tool_output_callback=tool_output_callback,\n            api_response_callback=api_response_callback,\n            api_key=\"test-key\",\n            tool_version=\"computer_use_20250124\",\n        )\n\n        assert len(result) == 4\n        assert result[0] == {\"role\": \"user\", \"content\": \"Test message\"}\n        assert result[1][\"role\"] == \"assistant\"\n        assert result[2][\"role\"] == \"user\"\n        assert result[3][\"role\"] == \"assistant\"\n\n        assert client.beta.messages.with_raw_response.create.call_count == 2\n        tool_collection.run.assert_called_once_with(\n            name=\"computer\", tool_input={\"action\": \"test\"}\n        )\n        output_callback.assert_called_with(\n            BetaTextBlockParam(text=\"Done!\", type=\"text\", citations=None)\n        )\n        assert output_callback.call_count == 3\n        assert tool_output_callback.call_count == 1\n        assert api_response_callback.call_count == 2\n"
  },
  {
    "path": "computer-use-demo/tests/streamlit_test.py",
    "content": "from unittest import mock\n\nimport pytest\nfrom anthropic.types import TextBlockParam\nfrom streamlit.testing.v1 import AppTest\n\nfrom computer_use_demo.streamlit import Sender\n\n\n@pytest.fixture\ndef streamlit_app():\n    return AppTest.from_file(\"computer_use_demo/streamlit.py\")\n\n\ndef test_streamlit(streamlit_app: AppTest):\n    streamlit_app.run()\n    streamlit_app.text_input[1].set_value(\"sk-ant-0000000000000\").run()\n    with mock.patch(\"computer_use_demo.loop.sampling_loop\") as patch:\n        streamlit_app.chat_input[0].set_value(\"Hello\").run()\n        assert patch.called\n        assert patch.call_args.kwargs[\"messages\"] == [\n            {\n                \"role\": Sender.USER,\n                \"content\": [TextBlockParam(text=\"Hello\", type=\"text\")],\n            }\n        ]\n        assert not streamlit_app.exception\n"
  },
  {
    "path": "computer-use-demo/tests/tools/bash_test.py",
    "content": "import pytest\n\nfrom computer_use_demo.tools.bash import BashTool20241022, BashTool20250124, ToolError\n\n\n@pytest.fixture(params=[BashTool20241022, BashTool20250124])\ndef bash_tool(request):\n    return request.param()\n\n\n@pytest.mark.asyncio\nasync def test_bash_tool_restart(bash_tool):\n    result = await bash_tool(restart=True)\n    assert result.system == \"tool has been restarted.\"\n\n    # Verify the tool can be used after restart\n    result = await bash_tool(command=\"echo 'Hello after restart'\")\n    assert \"Hello after restart\" in result.output\n\n\n@pytest.mark.asyncio\nasync def test_bash_tool_run_command(bash_tool):\n    result = await bash_tool(command=\"echo 'Hello, World!'\")\n    assert result.output.strip() == \"Hello, World!\"\n    assert result.error == \"\"\n\n\n@pytest.mark.asyncio\nasync def test_bash_tool_no_command(bash_tool):\n    with pytest.raises(ToolError, match=\"no command provided.\"):\n        await bash_tool()\n\n\n@pytest.mark.asyncio\nasync def test_bash_tool_session_creation(bash_tool):\n    result = await bash_tool(command=\"echo 'Session created'\")\n    assert bash_tool._session is not None\n    assert \"Session created\" in result.output\n\n\n@pytest.mark.asyncio\nasync def test_bash_tool_session_reuse(bash_tool):\n    result1 = await bash_tool(command=\"echo 'First command'\")\n    result2 = await bash_tool(command=\"echo 'Second command'\")\n\n    assert \"First command\" in result1.output\n    assert \"Second command\" in result2.output\n\n\n@pytest.mark.asyncio\nasync def test_bash_tool_session_error(bash_tool):\n    result = await bash_tool(command=\"invalid_command_that_does_not_exist\")\n    assert \"command not found\" in result.error\n\n\n@pytest.mark.asyncio\nasync def test_bash_tool_non_zero_exit(bash_tool):\n    result = await bash_tool(command=\"bash -c 'exit 1'\")\n    assert result.error.strip() == \"\"\n    assert result.output.strip() == \"\"\n\n\n@pytest.mark.asyncio\nasync def test_bash_tool_timeout(bash_tool):\n    await bash_tool(command=\"echo 'Hello, World!'\")\n    bash_tool._session._timeout = 0.1  # Set a very short timeout for testing\n    with pytest.raises(\n        ToolError,\n        match=\"timed out: bash has not returned in 0.1 seconds and must be restarted\",\n    ):\n        await bash_tool(command=\"sleep 1\")\n"
  },
  {
    "path": "computer-use-demo/tests/tools/computer_test.py",
    "content": "from unittest.mock import AsyncMock, patch\n\nimport pytest\n\nfrom computer_use_demo.tools.computer import (\n    ComputerTool20241022,\n    ComputerTool20250124,\n    ScalingSource,\n    ToolError,\n    ToolResult,\n)\n\n\n@pytest.fixture(params=[ComputerTool20241022, ComputerTool20250124])\ndef computer_tool(request):\n    return request.param()\n\n\n@pytest.mark.asyncio\nasync def test_computer_tool_mouse_move(computer_tool):\n    with patch.object(computer_tool, \"shell\", new_callable=AsyncMock) as mock_shell:\n        mock_shell.return_value = ToolResult(output=\"Mouse moved\")\n        result = await computer_tool(action=\"mouse_move\", coordinate=[100, 200])\n        mock_shell.assert_called_once_with(\n            f\"{computer_tool.xdotool} mousemove --sync 100 200\"\n        )\n        assert result.output == \"Mouse moved\"\n\n\n@pytest.mark.asyncio\nasync def test_computer_tool_type(computer_tool):\n    with (\n        patch.object(computer_tool, \"shell\", new_callable=AsyncMock) as mock_shell,\n        patch.object(\n            computer_tool, \"screenshot\", new_callable=AsyncMock\n        ) as mock_screenshot,\n    ):\n        mock_shell.return_value = ToolResult(output=\"Text typed\")\n        mock_screenshot.return_value = ToolResult(base64_image=\"base64_screenshot\")\n        result = await computer_tool(action=\"type\", text=\"Hello, World!\")\n        assert mock_shell.call_count == 1\n        assert \"type --delay 12 -- 'Hello, World!'\" in mock_shell.call_args[0][0]\n        assert result.output == \"Text typed\"\n        assert result.base64_image == \"base64_screenshot\"\n\n\n@pytest.mark.asyncio\nasync def test_computer_tool_screenshot(computer_tool):\n    with patch.object(\n        computer_tool, \"screenshot\", new_callable=AsyncMock\n    ) as mock_screenshot:\n        mock_screenshot.return_value = ToolResult(base64_image=\"base64_screenshot\")\n        result = await computer_tool(action=\"screenshot\")\n        mock_screenshot.assert_called_once()\n        assert result.base64_image == \"base64_screenshot\"\n\n\n@pytest.mark.asyncio\nasync def test_computer_tool_scaling(computer_tool):\n    computer_tool._scaling_enabled = True\n    computer_tool.width = 1920\n    computer_tool.height = 1080\n\n    # Test scaling from API to computer\n    x, y = computer_tool.scale_coordinates(ScalingSource.API, 1366, 768)\n    assert x == 1920\n    assert y == 1080\n\n    # Test scaling from computer to API\n    x, y = computer_tool.scale_coordinates(ScalingSource.COMPUTER, 1920, 1080)\n    assert x == 1366\n    assert y == 768\n\n    # Test no scaling when disabled\n    computer_tool._scaling_enabled = False\n    x, y = computer_tool.scale_coordinates(ScalingSource.API, 1366, 768)\n    assert x == 1366\n    assert y == 768\n\n\n@pytest.mark.asyncio\nasync def test_computer_tool_scaling_with_different_aspect_ratio(computer_tool):\n    computer_tool._scaling_enabled = True\n    computer_tool.width = 1920\n    computer_tool.height = 1200  # 16:10 aspect ratio\n\n    # Test scaling from API to computer\n    x, y = computer_tool.scale_coordinates(ScalingSource.API, 1280, 800)\n    assert x == 1920\n    assert y == 1200\n\n    # Test scaling from computer to API\n    x, y = computer_tool.scale_coordinates(ScalingSource.COMPUTER, 1920, 1200)\n    assert x == 1280\n    assert y == 800\n\n\n@pytest.mark.asyncio\nasync def test_computer_tool_no_scaling_for_unsupported_resolution(computer_tool):\n    computer_tool._scaling_enabled = True\n    computer_tool.width = 4096\n    computer_tool.height = 2160\n\n    # Test no scaling for unsupported resolution\n    x, y = computer_tool.scale_coordinates(ScalingSource.API, 4096, 2160)\n    assert x == 4096\n    assert y == 2160\n\n    x, y = computer_tool.scale_coordinates(ScalingSource.COMPUTER, 4096, 2160)\n    assert x == 4096\n    assert y == 2160\n\n\n@pytest.mark.asyncio\nasync def test_computer_tool_scaling_out_of_bounds(computer_tool):\n    computer_tool._scaling_enabled = True\n    computer_tool.width = 1920\n    computer_tool.height = 1080\n\n    # Test scaling from API with out of bounds coordinates\n    with pytest.raises(ToolError, match=\"Coordinates .*, .* are out of bounds\"):\n        x, y = computer_tool.scale_coordinates(ScalingSource.API, 2000, 1500)\n\n\n@pytest.mark.asyncio\nasync def test_computer_tool_invalid_action(computer_tool):\n    with pytest.raises(ToolError, match=\"Invalid action: invalid_action\"):\n        await computer_tool(action=\"invalid_action\")\n\n\n@pytest.mark.asyncio\nasync def test_computer_tool_missing_coordinate(computer_tool):\n    with pytest.raises(ToolError, match=\"coordinate is required for mouse_move\"):\n        await computer_tool(action=\"mouse_move\")\n\n\n@pytest.mark.asyncio\nasync def test_computer_tool_missing_text(computer_tool):\n    with pytest.raises(ToolError, match=\"text is required for type\"):\n        await computer_tool(action=\"type\")\n"
  },
  {
    "path": "computer-use-demo/tests/tools/edit_test.py",
    "content": "from pathlib import Path\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom computer_use_demo.tools.base import CLIResult, ToolError, ToolResult\nfrom computer_use_demo.tools.edit import EditTool20250728\n\n\n@pytest.fixture\ndef edit_tool():\n    return EditTool20250728()\n\n\n@pytest.mark.asyncio\nasync def test_view_command(edit_tool):\n    # Test viewing a file that exists\n    with (\n        patch(\"pathlib.Path.exists\", return_value=True),\n        patch(\"pathlib.Path.is_dir\", return_value=False),\n        patch(\"pathlib.Path.read_text\") as mock_read_text,\n    ):\n        mock_read_text.return_value = \"File content\"\n        result = await edit_tool(command=\"view\", path=\"/test/file.txt\")\n        assert isinstance(result, CLIResult)\n        assert result.output\n        assert \"File content\" in result.output\n\n    # Test viewing a directory\n    with (\n        patch(\"pathlib.Path.exists\", return_value=True),\n        patch(\"pathlib.Path.is_dir\", return_value=True),\n        patch(\"computer_use_demo.tools.edit.run\") as mock_run,\n    ):\n        mock_run.return_value = (None, \"file1.txt\\nfile2.txt\", None)\n        result = await edit_tool(command=\"view\", path=\"/test/dir\")\n        assert isinstance(result, CLIResult)\n        assert result.output\n        assert \"file1.txt\" in result.output\n        assert \"file2.txt\" in result.output\n\n    # Test viewing a file with a specific range\n    with (\n        patch(\"pathlib.Path.exists\", return_value=True),\n        patch(\"pathlib.Path.is_dir\", return_value=False),\n        patch(\"pathlib.Path.read_text\") as mock_read_text,\n    ):\n        mock_read_text.return_value = \"Line 1\\nLine 2\\nLine 3\\nLine 4\"\n        result = await edit_tool(\n            command=\"view\", path=\"/test/file.txt\", view_range=[2, 3]\n        )\n        assert isinstance(result, CLIResult)\n        assert result.output\n        assert \"\\n     2\\tLine 2\\n     3\\tLine 3\\n\" in result.output\n\n    # Test viewing a file with an invalid range\n    with (\n        patch(\"pathlib.Path.exists\", return_value=True),\n        patch(\"pathlib.Path.is_dir\", return_value=False),\n        patch(\"pathlib.Path.read_text\") as mock_read_text,\n    ):\n        mock_read_text.return_value = \"Line 1\\nLine 2\\nLine 3\\nLine 4\"\n        with pytest.raises(ToolError, match=\"Invalid `view_range`\"):\n            await edit_tool(command=\"view\", path=\"/test/file.txt\", view_range=[3, 2])\n\n    # Test viewing a non-existent file\n    with patch(\"pathlib.Path.exists\", return_value=False):\n        with pytest.raises(ToolError, match=\"does not exist\"):\n            await edit_tool(command=\"view\", path=\"/nonexistent/file.txt\")\n\n    # Test viewing a directory with a view_range\n    with (\n        patch(\"pathlib.Path.exists\", return_value=True),\n        patch(\"pathlib.Path.is_dir\", return_value=True),\n    ):\n        with pytest.raises(ToolError, match=\"view_range` parameter is not allowed\"):\n            await edit_tool(command=\"view\", path=\"/test/dir\", view_range=[1, 2])\n\n\n@pytest.mark.asyncio\nasync def test_create_command(edit_tool):\n    # Test creating a new file with content\n    with (\n        patch(\"pathlib.Path.exists\", return_value=False),\n        patch(\"pathlib.Path.write_text\") as mock_write_text,\n    ):\n        result = await edit_tool(\n            command=\"create\", path=\"/test/newfile.txt\", file_text=\"New file content\"\n        )\n        assert isinstance(result, ToolResult)\n        assert result.output\n        assert \"File created successfully\" in result.output\n        mock_write_text.assert_called_once_with(\"New file content\")\n\n    # Test attempting to create a file without content\n    with patch(\"pathlib.Path.exists\", return_value=False):\n        with pytest.raises(ToolError, match=\"Parameter `file_text` is required\"):\n            await edit_tool(command=\"create\", path=\"/test/newfile.txt\")\n\n    # Test attempting to create a file that already exists\n    with patch(\"pathlib.Path.exists\", return_value=True):\n        with pytest.raises(ToolError, match=\"File already exists\"):\n            await edit_tool(\n                command=\"create\", path=\"/test/existingfile.txt\", file_text=\"Content\"\n            )\n\n\n@pytest.mark.asyncio\nasync def test_str_replace_command(edit_tool):\n    # Test replacing a unique string in a file\n    with (\n        patch(\"pathlib.Path.exists\", return_value=True),\n        patch(\"pathlib.Path.is_dir\", return_value=False),\n        patch(\"pathlib.Path.read_text\") as mock_read_text,\n        patch(\"pathlib.Path.write_text\") as mock_write_text,\n    ):\n        mock_read_text.return_value = \"Original content\"\n        result = await edit_tool(\n            command=\"str_replace\",\n            path=\"/test/file.txt\",\n            old_str=\"Original\",\n            new_str=\"New\",\n        )\n        assert isinstance(result, CLIResult)\n        assert result.output\n        assert \"has been edited\" in result.output\n        mock_write_text.assert_called_once_with(\"New content\")\n\n    # Test attempting to replace a non-existent string\n    with (\n        patch(\"pathlib.Path.exists\", return_value=True),\n        patch(\"pathlib.Path.is_dir\", return_value=False),\n        patch(\"pathlib.Path.read_text\") as mock_read_text,\n    ):\n        mock_read_text.return_value = \"Original content\"\n        with pytest.raises(ToolError, match=\"did not appear verbatim\"):\n            await edit_tool(\n                command=\"str_replace\",\n                path=\"/test/file.txt\",\n                old_str=\"Nonexistent\",\n                new_str=\"New\",\n            )\n\n    # Test attempting to replace a string that appears multiple times\n    with (\n        patch(\"pathlib.Path.exists\", return_value=True),\n        patch(\"pathlib.Path.is_dir\", return_value=False),\n        patch(\"pathlib.Path.read_text\") as mock_read_text,\n    ):\n        mock_read_text.return_value = \"Test test test\"\n        with pytest.raises(ToolError, match=\"Multiple occurrences\"):\n            await edit_tool(\n                command=\"str_replace\",\n                path=\"/test/file.txt\",\n                old_str=\"test\",\n                new_str=\"example\",\n            )\n\n    edit_tool._file_history.clear()\n    # Verify that the file history is updated after replacement\n    with (\n        patch(\"pathlib.Path.exists\", return_value=True),\n        patch(\"pathlib.Path.is_dir\", return_value=False),\n        patch(\"pathlib.Path.read_text\") as mock_read_text,\n        patch(\"pathlib.Path.write_text\"),\n    ):\n        mock_read_text.return_value = \"Original content\"\n        await edit_tool(\n            command=\"str_replace\",\n            path=\"/test/file.txt\",\n            old_str=\"Original\",\n            new_str=\"New\",\n        )\n        assert edit_tool._file_history[Path(\"/test/file.txt\")] == [\"Original content\"]\n\n\n@pytest.mark.asyncio\nasync def test_insert_command(edit_tool):\n    # Test inserting a string at a valid line number\n    with (\n        patch(\"pathlib.Path.exists\", return_value=True),\n        patch(\"pathlib.Path.is_dir\", return_value=False),\n        patch(\"pathlib.Path.read_text\") as mock_read_text,\n        patch(\"pathlib.Path.write_text\") as mock_write_text,\n    ):\n        mock_read_text.return_value = \"Line 1\\nLine 2\\nLine 3\"\n        result = await edit_tool(\n            command=\"insert\", path=\"/test/file.txt\", insert_line=2,\n            insert_text=\"New Line\",\n        )\n        assert isinstance(result, CLIResult)\n        assert result.output\n        assert \"has been edited\" in result.output\n        mock_write_text.assert_called_once_with(\"Line 1\\nLine 2\\nNew Line\\nLine 3\")\n\n    # Test inserting a string at the beginning of the file (line 0)\n    with (\n        patch(\"pathlib.Path.exists\", return_value=True),\n        patch(\"pathlib.Path.is_dir\", return_value=False),\n        patch(\"pathlib.Path.read_text\") as mock_read_text,\n        patch(\"pathlib.Path.write_text\") as mock_write_text,\n    ):\n        mock_read_text.return_value = \"Line 1\\nLine 2\"\n        result = await edit_tool(\n            command=\"insert\",\n            path=\"/test/file.txt\",\n            insert_line=0,\n            insert_text=\"New First Line\",\n        )\n        assert isinstance(result, CLIResult)\n        assert result.output\n        assert \"has been edited\" in result.output\n        mock_write_text.assert_called_once_with(\"New First Line\\nLine 1\\nLine 2\")\n\n    # Test inserting a string at the end of the file\n    with (\n        patch(\"pathlib.Path.exists\", return_value=True),\n        patch(\"pathlib.Path.is_dir\", return_value=False),\n        patch(\"pathlib.Path.read_text\") as mock_read_text,\n        patch(\"pathlib.Path.write_text\") as mock_write_text,\n    ):\n        mock_read_text.return_value = \"Line 1\\nLine 2\"\n        result = await edit_tool(\n            command=\"insert\",\n            path=\"/test/file.txt\",\n            insert_line=2,\n            insert_text=\"New Last Line\",\n        )\n        assert isinstance(result, CLIResult)\n        assert result.output\n        assert \"has been edited\" in result.output\n        mock_write_text.assert_called_once_with(\"Line 1\\nLine 2\\nNew Last Line\")\n\n    # Test attempting to insert at an invalid line number\n    with (\n        patch(\"pathlib.Path.exists\", return_value=True),\n        patch(\"pathlib.Path.is_dir\", return_value=False),\n        patch(\"pathlib.Path.read_text\") as mock_read_text,\n    ):\n        mock_read_text.return_value = \"Line 1\\nLine 2\"\n        with pytest.raises(ToolError, match=\"Invalid `insert_line` parameter\"):\n            await edit_tool(\n                command=\"insert\",\n                path=\"/test/file.txt\",\n                insert_line=5,\n                insert_text=\"Invalid Line\",\n            )\n\n    # Verify that the file history is updated after insertion\n    edit_tool._file_history.clear()\n    with (\n        patch(\"pathlib.Path.exists\", return_value=True),\n        patch(\"pathlib.Path.is_dir\", return_value=False),\n        patch(\"pathlib.Path.read_text\") as mock_read_text,\n        patch(\"pathlib.Path.write_text\"),\n    ):\n        mock_read_text.return_value = \"Original content\"\n        await edit_tool(\n            command=\"insert\", path=\"/test/file.txt\", insert_line=1,\n            insert_text=\"New Line\",\n        )\n        assert edit_tool._file_history[Path(\"/test/file.txt\")] == [\"Original content\"]\n\n\n@pytest.mark.asyncio\nasync def test_validate_path(edit_tool):\n    # Test with valid absolute paths\n    with (\n        patch(\"pathlib.Path.exists\", return_value=True),\n        patch(\"pathlib.Path.is_dir\", return_value=False),\n    ):\n        edit_tool.validate_path(\"view\", Path(\"/valid/path.txt\"))\n\n    # Test with relative paths (should raise an error)\n    with pytest.raises(ToolError, match=\"not an absolute path\"):\n        edit_tool.validate_path(\"view\", Path(\"relative/path.txt\"))\n\n    # Test with non-existent paths for non-create commands (should raise an error)\n    with patch(\"pathlib.Path.exists\", return_value=False):\n        with pytest.raises(ToolError, match=\"does not exist\"):\n            edit_tool.validate_path(\"view\", Path(\"/nonexistent/file.txt\"))\n\n    # Test with existing paths for create command (should raise an error)\n    with patch(\"pathlib.Path.exists\", return_value=True):\n        with pytest.raises(ToolError, match=\"File already exists\"):\n            edit_tool.validate_path(\"create\", Path(\"/existing/file.txt\"))\n\n    # Test with directory paths for non-view commands (should raise an error)\n    with (\n        patch(\"pathlib.Path.exists\", return_value=True),\n        patch(\"pathlib.Path.is_dir\", return_value=True),\n    ):\n        with pytest.raises(ToolError, match=\"is a directory\"):\n            edit_tool.validate_path(\"str_replace\", Path(\"/directory/path\"))\n\n    # Test with directory path for view command (should not raise an error)\n    with (\n        patch(\"pathlib.Path.exists\", return_value=True),\n        patch(\"pathlib.Path.is_dir\", return_value=True),\n    ):\n        edit_tool.validate_path(\"view\", Path(\"/directory/path\"))\n"
  },
  {
    "path": "customer-support-agent/.eslintrc.json",
    "content": "{\n  \"extends\": \"next/core-web-vitals\"\n}\n"
  },
  {
    "path": "customer-support-agent/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n.yarn/install-state.gz\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "customer-support-agent/README.md",
    "content": "# Claude Customer Support Agent\n\nAn advanced, fully customizable customer support chat interface powered by Claude and leveraging Amazon Bedrock Knowledge Bases for knowledge retrieval.\n![preview](tutorial/preview.png)\n\n## Key Features\n\n-  AI-powered chat using Anthropic's Claude model\n-  Amazon Bedrock integration for contextual knowledge retrieval\n-  Real-time thinking & debug information display\n-  Knowledge base source visualization\n-  User mood detection & appropriate agent redirection\n-  Highly customizable UI with shadcn/ui components\n\n##  Getting Started\n\n1. Clone this repository\n2. Install dependencies: `npm install`\n3. Set up your environment variables (see Configuration section)\n4. Run the development server: `npm run dev`\n5. Open [http://localhost:3000](http://localhost:3000) in your browser\n\n## ⚙️ Configuration\n\nCreate a `.env.local` file in the root directory with the following variables:\n\n```\nANTHROPIC_API_KEY=your_anthropic_api_key\nBAWS_ACCESS_KEY_ID=your_aws_access_key\nBAWS_SECRET_ACCESS_KEY=your_aws_secret_key\n```\n\nNote: We are adding a 'B' in front of the AWS environment variables for a reason that will be discussed later in the deployment section.\n\n##  How to Get Your Keys\n\n### Claude API Key\n\n1. Visit [console.anthropic.com](https://console.anthropic.com/dashboard)\n2. Sign up or log in to your account\n3. Click on \"Get API keys\"\n4. Copy the key and paste it into your `.env.local` file\n\n### AWS Access Key and Secret Key\n\nFollow these steps to obtain your AWS credentials:\n\n1. Log in to the AWS Management Console\n2. Navigate to the IAM (Identity and Access Management) dashboard\n\n3. In the left sidebar, click on \"Users\"\n\n4. Click \"Create user\" and follow the prompts to create a new user\n   ![Add User](tutorial/create-user.png)\n5. On the Set Permission page, select the \"Attach policies directly\" policy\n   ![Attach Policy](tutorial/attach.png)\n5. On the permissions page, use the \"AmazonBedrockFullAccess\" policy\n   ![Attach Policy](tutorial/bedrock.png)\n6. Review and create the user\n7. On the Summary page, click on Create access key.\n8. Then select \"Application running on an AWS compute service\". Add a description if desired, then click \"Create\".\n9. You will now see the Access Key ID and Secret Access Key displayed. Note that these keys are only visible once during creation, so be sure to save them securely.\n   ![Access Keys](tutorial/access-keys.png)\n8. Copy these keys and paste them into your `.env.local` file\n\nNote: Make sure to keep your keys secure and never share them publicly.\n\n\n##  Amazon Bedrock RAG Integration\n\nThis project utilizes Amazon Bedrock for Retrieval-Augmented Generation (RAG). To set up:\n\n1. Ensure you have an AWS account with Bedrock access.\n2. Create a Bedrock knowledge base in your desired AWS region.\n3. Index your documents/sources in the knowledge base. For more info on that, check the \"How to Create Your Own Knowledge Base\" section.\n4. In `ChatArea.tsx`, update the `knowledgeBases` array with your knowledge base IDs and names:\n\n```typescript\nconst knowledgeBases: KnowledgeBase[] = [\n  { id: \"your-knowledge-base-id\", name: \"Your KB Name\" },\n  // Add more knowledge bases as needed\n];\n```\n\nThe application will use these knowledge bases for context retrieval during conversations.\n\n### How to Create Your Own Knowledge Base\n\nTo create your own knowledge base:\n\n1. Go to your AWS Console and select Amazon Bedrock.\n2. In the left side menu, click on \"Knowledge base\" under \"More\".\n\n3. Click on \"Create knowledge base\".\n   ![Create Knowledge Base](tutorial/create-knowledge-base.png)\n4. Give your knowledge base a name. You can leave \"Create a new service role\".\n5. Choose a source for your knowledge base. In this example, we'll use Amazon S3 storage service.\n   ![Choose Source](tutorial/choose-source.png)\n\n   Note: If you're using the S3 storage service, you'll need to create a bucket first where you will upload your files. Alternatively, you can also upload your files after the creation of a knowledge base.\n\n6. Click \"Next\".\n7. Choose a location for your knowledge base. This can be S3 buckets, folders, or even single documents.\n8. Click \"Next\".\n9. Select your preferred embedding model. In this case, we'll use Titan Text Embeddings 2.\n10. Select \"Quick create a new vector store\".\n11. Confirm and create your knowledge base.\n12. Once you have done this, get your knowledge base ID from the knowledge base overview.\n\n\n##  Switching Models\n\nThis project supports multiple Claude models. To switch between models:\n\n1. In `ChatArea.tsx`, the `models` array defines available models:\n\n```typescript\nconst models: Model[] = [\n  { id: \"claude-3-haiku-20240307\", name: \"Claude 3 Haiku\" },\n  { id: \"claude-3-5-sonnet-20240620\", name: \"Claude 3.5 Sonnet\" },\n  // Add more models as needed\n];\n```\n\n2. The `selectedModel` state variable controls the currently selected model:\n\n```typescript\nconst [selectedModel, setSelectedModel] = useState(\"claude-3-haiku-20240307\");\n```\n\n3. To implement model switching in the UI, a dropdown component is used that updates the `selectedModel`.\n\n\n##  Customization\n\nThis project leverages shadcn/ui components, offering a high degree of customization:\n\n* Modify the UI components in the `components/ui` directory\n* Adjust the theme in `app/globals.css`\n* Customize the layout and functionality in individual component files\n* Modify the theme colors and styles by editing the `styles/themes.js` file:\n\n```javascript\n// styles/themes.js\nexport const themes = {\n  neutral: {\n    light: {\n      // Light mode colors for neutral theme\n    },\n    dark: {\n      // Dark mode colors for neutral theme\n    }\n  },\n  // Add more themes here\n};\n```\nYou can add new themes or modify existing ones by adjusting the color values in this file.\n\n##  Deploy with AWS Amplify\n\nTo deploy this application using AWS Amplify, follow these steps:\n\n1. Go to your AWS Console and select Amplify.\n2. Click on \"Create new app\" (image link to be added later).\n3. Select GitHub (or your preferred provider) as the source.\n4. Choose this repository.\n5. Edit the YAML file to contain:\n\n   ```yaml\n   version: 1\n   frontend:\n     phases:\n       preBuild:\n         commands:\n           - npm ci --cache .npm --prefer-offline\n       build:\n         commands:\n           - npm run build # Next.js build runs first\n           - echo \"ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY\" >> .env\n           - echo \"KNOWLEDGE_BASE_ID=$KNOWLEDGE_BASE_ID\" >> .env\n           - echo \"BAWS_ACCESS_KEY_ID=$BAWS_ACCESS_KEY_ID\" >> .env\n           - echo \"BAWS_SECRET_ACCESS_KEY=$BAWS_SECRET_ACCESS_KEY\" >> .env\n     artifacts:\n       baseDirectory: .next\n       files:\n         - \"**/*\"\n     cache:\n       paths:\n         - .next/cache/**/*\n         - .npm/**/*\n   ```\n\n6. Choose to create a new service role or use an existing one. Refer to the \"Service Role\" section for more information.\n7. Click on \"Advanced settings\" and add your environmental variables:\n\n   ```\n   ANTHROPIC_API_KEY=your_anthropic_api_key\n   BAWS_ACCESS_KEY_ID=your_aws_access_key\n   BAWS_SECRET_ACCESS_KEY=your_aws_secret_key\n   ```\n   The reason we are adding a 'B' in front of the keys here is because AWS doesn't allow keys in Amplify to start with \"AWS\".\n\n8. Click \"Save and deploy\" to start the deployment process.\n\nYour application will now be deployed using AWS Amplify.\n\n\n### Service Role\n\nOnce your application is deployed, if you selected to create a new service role:\n\n1. Go to your deployments page\n2. Select the deployment you just created\n3. Click on \"App settings\"\n4. Copy the Service role ARN\n5. Go to the IAM console and find this role\n6. Attach the \"AmazonBedrockFullAccess\" policy to the role\n\nThis ensures that your Amplify app has the necessary permissions to interact with Amazon Bedrock.\n\n##  Customized Deployment and Development\nThis project now supports flexible deployment and development configurations, allowing you to include or exclude specific components (left sidebar, right sidebar) based on your needs.\nConfiguration\nThe inclusion of sidebars is controlled by a config.ts file, which uses environment variables to set the configuration:\n```typescript\ntypescriptCopytype Config = {\n  includeLeftSidebar: boolean;\n  includeRightSidebar: boolean;\n};\n\nconst config: Config = {\n  includeLeftSidebar: process.env.NEXT_PUBLIC_INCLUDE_LEFT_SIDEBAR === \"true\",\n  includeRightSidebar: process.env.NEXT_PUBLIC_INCLUDE_RIGHT_SIDEBAR === \"true\",\n};\n\nexport default config;\n```\n\nThis configuration uses two environment variables:\n\nNEXT_PUBLIC_INCLUDE_LEFT_SIDEBAR: Set to \"true\" to include the left sidebar\nNEXT_PUBLIC_INCLUDE_RIGHT_SIDEBAR: Set to \"true\" to include the right sidebar\n\n## NPM Scripts\nThe package.json includes several new scripts for different configurations:\n\n```bash\nnpm run dev: Runs the full app with both sidebars (default)\nnpm run build: Builds the full app with both sidebars (default)\nnpm run dev:full: Same as npm run dev\nnpm run dev:left: Runs the app with only the left sidebar\nnpm run dev:right: Runs the app with only the right sidebar\nnpm run dev:chat: Runs the app with just the chat area (no sidebars)\nnpm run build:full: Same as npm run build\nnpm run build:left: Builds the app with only the left sidebar\nnpm run build:right: Builds the app with only the right sidebar\nnpm run build:chat: Builds the app with just the chat area (no sidebars)\n```\n\nUsage\nTo use a specific configuration:\n\nFor development: Run the desired script (e.g., npm run dev:left)\nFor production: Build with the desired script (e.g., npm run build:right)\n\nThese scripts set the appropriate environment variables before running or building the application, allowing you to easily switch between different configurations.\nThis flexibility allows you to tailor the application's layout to your specific needs, whether for testing, development, or production deployment.\n\n## Appendix\n\nThis project is a prototype and is provided on an \"as-is\" basis. It is not intended for production use and may contain bugs, errors, or inconsistencies. By using this prototype, you acknowledge and agree that:\n- The software is provided in a pre-release, beta, or trial form.\n- It may not be suitable for production or mission-critical environments.\n- The developers are not responsible for any issues, data loss, or damages resulting from its use.\n- No warranties or guarantees of any kind are provided, either expressed or implied.\n- Support for this prototype may be limited or unavailable.\n- Use of this prototype is at your own risk. We encourage you to report any issues or provide feedback to help improve future versions.\n"
  },
  {
    "path": "customer-support-agent/amplify.yml",
    "content": "version: 1\nfrontend:\n  phases:\n    preBuild:\n      commands:\n        - npm ci --cache .npm --prefer-offline\n    build:\n      commands:\n        - npm run build # Next.js build runs first\n        - echo \"ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY\" >> .env\n        - echo \"KNOWLEDGE_BASE_ID=$KNOWLEDGE_BASE_ID\" >> .env\n        - echo \"BAWS_ACCESS_KEY_ID=$BAWS_ACCESS_KEY_ID\" >> .env\n        - echo \"BAWS_SECRET_ACCESS_KEY=$BAWS_SECRET_ACCESS_KEY\" >> .env\n  artifacts:\n    baseDirectory: .next\n    files:\n      - \"**/*\"\n  cache:\n    paths:\n      - .next/cache/**/*\n      - .npm/**/*\n"
  },
  {
    "path": "customer-support-agent/app/api/chat/route.ts",
    "content": "import Anthropic from \"@anthropic-ai/sdk\";\nimport { z } from \"zod\";\nimport { retrieveContext, RAGSource } from \"@/app/lib/utils\";\nimport crypto from \"crypto\";\nimport customerSupportCategories from \"@/app/lib/customer_support_categories.json\";\n\nconst anthropic = new Anthropic({\n  apiKey: process.env.ANTHROPIC_API_KEY,\n});\n\n// Debug message helper function\n// Input: message string and optional data object\n// Output: JSON string with message, sanitized data, and timestamp\nconst debugMessage = (msg: string, data: any = {}) => {\n  console.log(msg, data);\n  const timestamp = new Date().toISOString().replace(/[^\\x20-\\x7E]/g, \"\");\n  const safeData = JSON.parse(JSON.stringify(data));\n  return JSON.stringify({ msg, data: safeData, timestamp });\n};\n\n// Define the schema for the AI response using Zod\n// This ensures type safety and validation for the AI's output\nconst responseSchema = z.object({\n  response: z.string(),\n  thinking: z.string(),\n  user_mood: z.enum([\n    \"positive\",\n    \"neutral\",\n    \"negative\",\n    \"curious\",\n    \"frustrated\",\n    \"confused\",\n  ]),\n  suggested_questions: z.array(z.string()),\n  debug: z.object({\n    context_used: z.boolean(),\n  }),\n  matched_categories: z.array(z.string()).optional(),\n  redirect_to_agent: z\n    .object({\n      should_redirect: z.boolean(),\n      reason: z.string().optional(),\n    })\n    .optional(),\n});\n\n// Helper function to sanitize header values\n// Input: string value\n// Output: sanitized string (ASCII characters only)\nfunction sanitizeHeaderValue(value: string): string {\n  return value.replace(/[^\\x00-\\x7F]/g, \"\");\n}\n\n// Helper function to log timestamps for performance measurement\n// Input: label string and start time\n// Output: Logs the duration for the labeled operation\nconst logTimestamp = (label: string, start: number) => {\n  const timestamp = new Date().toISOString();\n  const time = ((performance.now() - start) / 1000).toFixed(2);\n  console.log(`⏱️ [${timestamp}] ${label}: ${time}s`);\n};\n\n// Main POST request handler\nexport async function POST(req: Request) {\n  const apiStart = performance.now();\n  const measureTime = (label: string) => logTimestamp(label, apiStart);\n\n  // Extract data from the request body\n  const { messages, model, knowledgeBaseId } = await req.json();\n  const latestMessage = messages[messages.length - 1].content;\n\n  console.log(\"📝 Latest Query:\", latestMessage);\n  measureTime(\"User Input Received\");\n\n  // Prepare debug data\n  const MAX_DEBUG_LENGTH = 1000;\n  const debugData = sanitizeHeaderValue(\n    debugMessage(\"🚀 API route called\", {\n      messagesReceived: messages.length,\n      latestMessageLength: latestMessage.length,\n      anthropicKeySlice: process.env.ANTHROPIC_API_KEY?.slice(0, 4) + \"****\",\n    }),\n  ).slice(0, MAX_DEBUG_LENGTH);\n\n  // Initialize variables for RAG retrieval\n  let retrievedContext = \"\";\n  let isRagWorking = false;\n  let ragSources: RAGSource[] = [];\n\n  // Attempt to retrieve context from RAG\n  try {\n    console.log(\"🔍 Initiating RAG retrieval for query:\", latestMessage);\n    measureTime(\"RAG Start\");\n    const result = await retrieveContext(latestMessage, knowledgeBaseId);\n    retrievedContext = result.context;\n    isRagWorking = result.isRagWorking;\n    ragSources = result.ragSources || [];\n\n    if (!result.isRagWorking) {\n      console.warn(\"🚨 RAG Retrieval failed but did not throw!\");\n    }\n\n    measureTime(\"RAG Complete\");\n    console.log(\"🔍 RAG Retrieved:\", isRagWorking ? \"YES\" : \"NO\");\n    console.log(\n      \"✅ RAG retrieval completed successfully. Context:\",\n      retrievedContext.slice(0, 100) + \"...\",\n    );\n  } catch (error) {\n    console.error(\"💀 RAG Error:\", error);\n    console.error(\"❌ RAG retrieval failed for query:\", latestMessage);\n    retrievedContext = \"\";\n    isRagWorking = false;\n    ragSources = [];\n  }\n\n  measureTime(\"RAG Total Duration\");\n\n  // Prepare categories context for the system prompt\n  const USE_CATEGORIES = true;\n  const categoryListString = customerSupportCategories.categories\n    .map((c) => c.id)\n    .join(\", \");\n\n  const categoriesContext = USE_CATEGORIES\n    ? `\n    To help with our internal classification of inquiries, we would like you to categorize inquiries in addition to answering them. We have provided you with ${customerSupportCategories.categories.length} customer support categories.\n    Check if your response fits into any category and include the category IDs in your \"matched_categories\" array.\n    The available categories are: ${categoryListString}\n    If multiple categories match, include multiple category IDs. If no categories match, return an empty array.\n  `\n    : \"\";\n\n  // Change the system prompt company for your use case\n  const systemPrompt = `You are acting as an Anthropic customer support assistant chatbot inside a chat window on a website. You are chatting with a human user who is asking for help about Anthropic's products and services. When responding to the user, aim to provide concise and helpful responses while maintaining a polite and professional tone.\n\n  To help you answer the user's question, we have retrieved the following information for you. It may or may not be relevant (we are using a RAG pipeline to retrieve this information):\n  ${isRagWorking ? `${retrievedContext}` : \"No information found for this query.\"}\n\n  Please provide responses that only use the information you have been given. If no information is available or if the information is not relevant for answering the question, you can redirect the user to a human agent for further assistance.\n\n  ${categoriesContext}\n\n  If the question is unrelated to Anthropic's products and services, you should redirect the user to a human agent.\n\n  You are the first point of contact for the user and should try to resolve their issue or provide relevant information. If you are unable to help the user or if the user explicitly asks to talk to a human, you can redirect them to a human agent for further assistance.\n  \n  To display your responses correctly, you must format your entire response as a valid JSON object with the following structure:\n  {\n      \"thinking\": \"Brief explanation of your reasoning for how you should address the user's query\",\n      \"response\": \"Your concise response to the user\",\n      \"user_mood\": \"positive|neutral|negative|curious|frustrated|confused\",\n      \"suggested_questions\": [\"Question 1?\", \"Question 2?\", \"Question 3?\"],\n      \"debug\": {\n        \"context_used\": true|false\n      },\n      ${USE_CATEGORIES ? '\"matched_categories\": [\"category_id1\", \"category_id2\"],' : \"\"}\n      \"redirect_to_agent\": {\n        \"should_redirect\": boolean,\n        \"reason\": \"Reason for redirection (optional, include only if should_redirect is true)\"\n      }\n    }\n\n  Here are a few examples of how your response should look like:\n\n  Example of a response without redirection to a human agent:\n  {\n    \"thinking\": \"Providing relevant information from the knowledge base\",\n    \"response\": \"Here's the information you requested...\",\n    \"user_mood\": \"curious\",\n    \"suggested_questions\": [\"How do I update my account?\", \"What are the payment options?\"],\n    \"debug\": {\n      \"context_used\": true\n    },\n    \"matched_categories\": [\"account_management\", \"billing\"],\n    \"redirect_to_agent\": {\n      \"should_redirect\": false\n    }\n  }\n\n  Example of a response with redirection to a human agent:\n  {\n    \"thinking\": \"User request requires human intervention\",\n    \"response\": \"I understand this is a complex issue. Let me connect you with a human agent who can assist you better.\",\n    \"user_mood\": \"frustrated\",\n    \"suggested_questions\": [],\n    \"debug\": {\n      \"context_used\": false\n    },\n    \"matched_categories\": [\"technical_support\"],\n    \"redirect_to_agent\": {\n      \"should_redirect\": true,\n      \"reason\": \"Complex technical issue requiring human expertise\"\n    }\n  }\n  `\n\n  function sanitizeAndParseJSON(jsonString : string) {\n    // Replace newlines within string values\n    const sanitized = jsonString.replace(/(?<=:\\s*\")(.|\\n)*?(?=\")/g, match => \n      match.replace(/\\n/g, \"\\\\n\")\n    );\n  \n    try {\n      return JSON.parse(sanitized);\n    } catch (parseError) {\n      console.error(\"Error parsing JSON response:\", parseError);\n      throw new Error(\"Invalid JSON response from AI\");\n    }\n  }\n\n  try {\n    console.log(`🚀 Query Processing`);\n    measureTime(\"Claude Generation Start\");\n\n    const anthropicMessages = messages.map((msg: any) => ({\n      role: msg.role,\n      content: msg.content,\n    }));\n\n    anthropicMessages.push({\n      role: \"assistant\",\n      content: \"{\",\n    });\n\n    const response = await anthropic.messages.create({\n      model: model,\n      max_tokens: 1000,\n      messages: anthropicMessages,\n      system: systemPrompt,\n      temperature: 0.3,\n    });\n\n    measureTime(\"Claude Generation Complete\");\n    console.log(\"✅ Message generation completed\");\n\n    // Extract text content from the response\n    const textContent = \"{\" + response.content\n      .filter((block): block is Anthropic.TextBlock => block.type === \"text\")\n      .map((block) => block.text)\n      .join(\" \");\n\n    // Parse the JSON response\n    let parsedResponse;\n    try {\n      parsedResponse = sanitizeAndParseJSON(textContent);\n    } catch (parseError) {\n      console.error(\"Error parsing JSON response:\", parseError);\n      throw new Error(\"Invalid JSON response from AI\");\n    }\n\n    const validatedResponse = responseSchema.parse(parsedResponse);\n\n    const responseWithId = {\n      id: crypto.randomUUID(),\n      ...validatedResponse,\n    };\n\n    // Check if redirection to a human agent is needed\n    if (responseWithId.redirect_to_agent?.should_redirect) {\n      console.log(\"🚨 AGENT REDIRECT TRIGGERED!\");\n      console.log(\"Reason:\", responseWithId.redirect_to_agent.reason);\n    }\n\n    // Prepare the response object\n    const apiResponse = new Response(JSON.stringify(responseWithId), {\n      status: 200,\n      headers: {\n        \"Content-Type\": \"application/json\",\n      },\n    });\n\n    // Add RAG sources to the response headers if available\n    if (ragSources.length > 0) {\n      apiResponse.headers.set(\n        \"x-rag-sources\",\n        sanitizeHeaderValue(JSON.stringify(ragSources)),\n      );\n    }\n\n    // Add debug data to the response headers\n    apiResponse.headers.set(\"X-Debug-Data\", sanitizeHeaderValue(debugData));\n\n    measureTime(\"API Complete\");\n\n    return apiResponse;\n  } catch (error) {\n    // Handle errors in AI response generation\n    console.error(\"💥 Error in message generation:\", error);\n    const errorResponse = {\n      response:\n        \"Sorry, there was an issue processing your request. Please try again later.\",\n      thinking: \"Error occurred during message generation.\",\n      user_mood: \"neutral\",\n      debug: { context_used: false },\n    };\n    return new Response(JSON.stringify(errorResponse), {\n      status: 500,\n      headers: { \"Content-Type\": \"application/json\" },\n    });\n  }\n}\n"
  },
  {
    "path": "customer-support-agent/app/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n    :root {\n        --background: 0 0% 100%;\n        --foreground: 0 0% 3.9%;\n        --card: 0 0% 100%;\n        --card-foreground: 0 0% 3.9%;\n        --popover: 0 0% 100%;\n        --popover-foreground: 0 0% 3.9%;\n        --primary: 0 0% 9%;\n        --primary-foreground: 0 0% 98%;\n        --secondary: 0 0% 96.1%;\n        --secondary-foreground: 0 0% 9%;\n        --muted: 0 0% 96.1%;\n        --muted-foreground: 0 0% 45.1%;\n        --accent: 0 0% 96.1%;\n        --accent-foreground: 0 0% 9%;\n        --destructive: 0 84.2% 60.2%;\n        --destructive-foreground: 0 0% 98%;\n        --border: 0 0% 89.8%;\n        --input: 0 0% 89.8%;\n        --ring: 0 0% 3.9%;\n        --radius: 0.75rem;\n        --chart-1: 12 76% 61%;\n        --chart-2: 173 58% 39%;\n        --chart-3: 197 37% 24%;\n        --chart-4: 43 74% 66%;\n        --chart-5: 27 87% 67%;\n    }\n\n    .dark {\n        --background: 0 0% 3.9%;\n        --foreground: 0 0% 98%;\n        --card: 0 0% 3.9%;\n        --card-foreground: 0 0% 98%;\n        --popover: 0 0% 3.9%;\n        --popover-foreground: 0 0% 98%;\n        --primary: 0 0% 98%;\n        --primary-foreground: 0 0% 9%;\n        --secondary: 0 0% 14.9%;\n        --secondary-foreground: 0 0% 98%;\n        --muted: 0 0% 14.9%;\n        --muted-foreground: 0 0% 63.9%;\n        --accent: 0 0% 14.9%;\n        --accent-foreground: 0 0% 98%;\n        --destructive: 0 62.8% 30.6%;\n        --destructive-foreground: 0 0% 98%;\n        --border: 0 0% 14.9%;\n        --input: 0 0% 14.9%;\n        --ring: 0 0% 83.1%;\n        --chart-1: 220 70% 50%;\n        --chart-2: 160 60% 45%;\n        --chart-3: 30 80% 55%;\n        --chart-4: 280 65% 60%;\n        --chart-5: 340 75% 55%;\n    }\n}\n\n@layer base {\n    * {\n        @apply border-border;\n    }\n    body {\n        @apply bg-background text-foreground;\n    }\n}\n"
  },
  {
    "path": "customer-support-agent/app/layout.tsx",
    "content": "import React from \"react\";\nimport type { Metadata } from \"next\";\nimport { Inter } from \"next/font/google\";\nimport \"./globals.css\";\nimport { ThemeProvider } from \"@/components/theme-provider\";\n\nconst inter = Inter({ subsets: [\"latin\"] });\n\nexport const metadata: Metadata = {\n  title: \"AI Chat Assistant\",\n  description: \"Chat with an AI assistant powered by Anthropic\",\n};\n\nexport default function RootLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  return (\n    <html lang=\"en\" suppressHydrationWarning>\n      <body className={`${inter.className} flex flex-col h-full`}>\n        <ThemeProvider\n          attribute=\"class\"\n          defaultTheme=\"system\"\n          enableSystem\n          disableTransitionOnChange\n        >\n          {children}\n        </ThemeProvider>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "customer-support-agent/app/lib/customer_support_categories.json",
    "content": "{\n  \"categories\": [\n    {\n      \"id\": \"account\",\n      \"name\": \"Account\",\n      \"keywords\": [\n        \"ban\",\n        \"appeal\",\n        \"deletion\",\n        \"logging in\",\n        \"phone verification\",\n        \"roles\",\n        \"permissions\"\n      ]\n    },\n    {\n      \"id\": \"billing\",\n      \"name\": \"Billing\",\n      \"keywords\": [\n        \"AR\",\n        \"billing details\",\n        \"invoice\",\n        \"receipt\",\n        \"payment method\",\n        \"pricing\",\n        \"refund request\",\n        \"subscription management\",\n        \"taxes\"\n      ]\n    },\n    {\n      \"id\": \"feature\",\n      \"name\": \"Feature\",\n      \"keywords\": [\n        \"functionality\",\n        \"tools\",\n        \"capabilities\",\n        \"options\",\n        \"modules\",\n        \"features\"\n      ]\n    },\n    {\n      \"id\": \"internal\",\n      \"name\": \"Internal\",\n      \"keywords\": [\n        \"auto-reply\",\n        \"forward\",\n        \"ignore\",\n        \"spam\",\n        \"other\",\n        \"sales reroute\"\n      ]\n    },\n    {\n      \"id\": \"legal\",\n      \"name\": \"Legal\",\n      \"keywords\": [\"data privacy\", \"policy\", \"terms of service\", \"usage policy\"]\n    },\n    {\n      \"id\": \"other\",\n      \"name\": \"Other\",\n      \"keywords\": [\"job inquiry\", \"3rd party company\"]\n    },\n    {\n      \"id\": \"technical\",\n      \"name\": \"Technical\",\n      \"keywords\": [\n        \"guides\",\n        \"issue\",\n        \"security\",\n        \"downtime\",\n        \"performance\",\n        \"interface\",\n        \"system\"\n      ]\n    },\n    {\n      \"id\": \"usage\",\n      \"name\": \"Usage\",\n      \"keywords\": [\n        \"errors\",\n        \"capabilities\",\n        \"quality\",\n        \"techniques\",\n        \"limits\",\n        \"availability\"\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "customer-support-agent/app/lib/utils.ts",
    "content": "import {\n  BedrockAgentRuntimeClient,\n  RetrieveCommand,\n  RetrieveCommandInput,\n} from \"@aws-sdk/client-bedrock-agent-runtime\";\nimport { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nconsole.log(\"🔑 Have AWS AccessKey?\", !!process.env.BAWS_ACCESS_KEY_ID);\nconsole.log(\"🔑 Have AWS Secret?\", !!process.env.BAWS_SECRET_ACCESS_KEY);\n\nconst bedrockClient = new BedrockAgentRuntimeClient({\n  region: \"us-east-1\", // Make sure this matches your Bedrock region\n  credentials: {\n    accessKeyId: process.env.BAWS_ACCESS_KEY_ID!,\n    secretAccessKey: process.env.BAWS_SECRET_ACCESS_KEY!,\n  },\n});\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\nexport interface RAGSource {\n  id: string;\n  fileName: string;\n  snippet: string;\n  score: number;\n}\n\nexport async function retrieveContext(\n  query: string,\n  knowledgeBaseId: string,\n  n: number = 3,\n): Promise<{\n  context: string;\n  isRagWorking: boolean;\n  ragSources: RAGSource[];\n}> {\n  try {\n    if (!knowledgeBaseId) {\n      console.error(\"knowledgeBaseId is not provided\");\n      return {\n        context: \"\",\n        isRagWorking: false,\n        ragSources: [],\n      };\n    }\n\n    const input: RetrieveCommandInput = {\n      knowledgeBaseId: knowledgeBaseId,\n      retrievalQuery: { text: query },\n      retrievalConfiguration: {\n        vectorSearchConfiguration: { numberOfResults: n },\n      },\n    };\n\n    const command = new RetrieveCommand(input);\n    const response = await bedrockClient.send(command);\n\n    // Parse results\n    const rawResults = response?.retrievalResults || [];\n    const ragSources: RAGSource[] = rawResults\n      .filter((res: any) => res.content && res.content.text)\n      .map((result: any, index: number) => {\n        const uri = result?.location?.s3Location?.uri || \"\";\n        const fileName = uri.split(\"/\").pop() || `Source-${index}.txt`;\n\n        return {\n          id:\n            result.metadata?.[\"x-amz-bedrock-kb-chunk-id\"] || `chunk-${index}`,\n          fileName: fileName.replace(/_/g, \" \").replace(\".txt\", \"\"),\n          snippet: result.content?.text || \"\",\n          score: result.score || 0,\n        };\n      })\n      .slice(0, 1);\n\n    console.log(\"🔍 Parsed RAG Sources:\", ragSources); // Debug log\n\n    const context = rawResults\n      .filter((res: any) => res.content && res.content.text)\n      .map((res: any) => res.content.text)\n      .join(\"\\n\\n\");\n\n    return {\n      context,\n      isRagWorking: true,\n      ragSources,\n    };\n  } catch (error) {\n    console.error(\"RAG Error:\", error);\n    return { context: \"\", isRagWorking: false, ragSources: [] };\n  }\n}\n"
  },
  {
    "path": "customer-support-agent/app/page.tsx",
    "content": "import React from \"react\";\nimport dynamic from \"next/dynamic\";\nimport TopNavBar from \"@/components/TopNavBar\";\nimport ChatArea from \"@/components/ChatArea\";\nimport config from \"@/config\";\n\nconst LeftSidebar = dynamic(() => import(\"@/components/LeftSidebar\"), {\n  ssr: false,\n});\nconst RightSidebar = dynamic(() => import(\"@/components/RightSidebar\"), {\n  ssr: false,\n});\n\nexport default function Home() {\n  return (\n    <div className=\"flex flex-col h-screen w-full\">\n      <TopNavBar />\n      <div className=\"flex flex-1 overflow-hidden h-screen w-full\">\n        {config.includeLeftSidebar && <LeftSidebar />}\n        <ChatArea />\n        {config.includeRightSidebar && <RightSidebar />}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "customer-support-agent/components/ChatArea.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport config from \"@/config\";\nimport { Button } from \"@/components/ui/button\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport ReactMarkdown from \"react-markdown\";\nimport rehypeHighlight from \"rehype-highlight\";\nimport rehypeRaw from \"rehype-raw\";\nimport {\n  HandHelping,\n  WandSparkles,\n  LifeBuoyIcon,\n  BookOpenText,\n  ChevronDown,\n  Send,\n} from \"lucide-react\";\nimport \"highlight.js/styles/atom-one-dark.css\";\nimport { Card, CardContent, CardFooter } from \"@/components/ui/card\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport Image from \"next/image\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\n\nconst TypedText = ({ text = \"\", delay = 5 }) => {\n  const [displayedText, setDisplayedText] = useState(\"\");\n\n  useEffect(() => {\n    if (!text) return;\n    const timer = setTimeout(() => {\n      setDisplayedText(text.substring(0, displayedText.length + 1));\n    }, delay);\n    return () => clearTimeout(timer);\n  }, [text, displayedText, delay]);\n\n  return <>{displayedText}</>;\n};\n\ntype ThinkingContent = {\n  id: string;\n  content: string;\n  user_mood: string;\n  debug: any;\n  matched_categories?: string[];\n};\n\ninterface ConversationHeaderProps {\n  selectedModel: string;\n  setSelectedModel: (modelId: string) => void;\n  models: Model[];\n  showAvatar: boolean;\n}\n\nconst UISelector = ({\n  redirectToAgent,\n}: {\n  redirectToAgent: { should_redirect: boolean; reason: string };\n}) => {\n  if (redirectToAgent.should_redirect) {\n    return (\n      <Button\n        size=\"sm\"\n        className=\"mt-2 flex items-center space-x-2\"\n        onClick={() => {\n          console.log(\"🔥 Human Agent Connection Requested!\", redirectToAgent);\n          const event = new CustomEvent(\"humanAgentRequested\", {\n            detail: {\n              reason: redirectToAgent.reason || \"Unknown\",\n              mood: \"frustrated\",\n              timestamp: new Date().toISOString(),\n            },\n          });\n          window.dispatchEvent(event);\n        }}\n      >\n        <LifeBuoyIcon className=\"w-4 h-4\" />\n        <small className=\"text-sm leading-none\">Talk to a human</small>\n      </Button>\n    );\n  }\n\n  return null;\n};\n\nconst SuggestedQuestions = ({\n  questions,\n  onQuestionClick,\n  isLoading,\n}: {\n  questions: string[];\n  onQuestionClick: (question: string) => void;\n  isLoading: boolean;\n}) => {\n  if (!questions || questions.length === 0) return null;\n\n  return (\n    <div className=\"mt-2 pl-10\">\n      {questions.map((question, index) => (\n        <Button\n          key={index}\n          className=\"text-sm mb-2 mr-2 ml-0 text-gray-500 shadow-sm\"\n          variant=\"outline\"\n          size=\"sm\"\n          onClick={() => onQuestionClick(question)}\n          disabled={isLoading}\n        >\n          {question}\n        </Button>\n      ))}\n    </div>\n  );\n};\n\nconst MessageContent = ({\n  content,\n  role,\n}: {\n  content: string;\n  role: string;\n}) => {\n  const [thinking, setThinking] = useState(true);\n  const [parsed, setParsed] = useState<{\n    response?: string;\n    thinking?: string;\n    user_mood?: string;\n    suggested_questions?: string[];\n    redirect_to_agent?: { should_redirect: boolean; reason: string };\n    debug?: {\n      context_used: boolean;\n    };\n  }>({});\n  const [error, setError] = useState(false);\n\n  useEffect(() => {\n    if (!content || role !== \"assistant\") return;\n\n    const timer = setTimeout(() => {\n      setError(true);\n      setThinking(false);\n    }, 30000);\n\n    try {\n      const result = JSON.parse(content);\n      console.log(\"🔍 Parsed Result:\", result);\n\n      if (\n        result.response &&\n        result.response.length > 0 &&\n        result.response !== \"...\"\n      ) {\n        setParsed(result);\n        setThinking(false);\n        clearTimeout(timer);\n      }\n    } catch (error) {\n      console.error(\"Error parsing JSON:\", error);\n      setError(true);\n      setThinking(false);\n    }\n\n    return () => clearTimeout(timer);\n  }, [content, role]);\n\n  if (thinking && role === \"assistant\") {\n    return (\n      <div className=\"flex items-center\">\n        <div className=\"animate-spin rounded-full h-4 w-4 border-b-2 border-gray-900 mr-2\" />\n        <span>Thinking...</span>\n      </div>\n    );\n  }\n\n  if (error && !parsed.response) {\n    return <div>Something went wrong. Please try again.</div>;\n  }\n\n  return (\n    <>\n      <ReactMarkdown rehypePlugins={[rehypeRaw, rehypeHighlight]}>\n        {parsed.response || content}\n      </ReactMarkdown>\n      {parsed.redirect_to_agent && (\n        <UISelector redirectToAgent={parsed.redirect_to_agent} />\n      )}\n    </>\n  );\n};\n\n// Define a type for the model\ntype Model = {\n  id: string;\n  name: string;\n};\n\ninterface Message {\n  id: string;\n  role: string;\n  content: string;\n}\n\n// Define the props interface for ConversationHeader\ninterface ConversationHeaderProps {\n  selectedModel: string;\n  setSelectedModel: (modelId: string) => void;\n  models: Model[];\n  showAvatar: boolean;\n  selectedKnowledgeBase: string;\n  setSelectedKnowledgeBase: (knowledgeBaseId: string) => void;\n  knowledgeBases: KnowledgeBase[];\n}\n\ntype KnowledgeBase = {\n  id: string;\n  name: string;\n};\n\nconst ConversationHeader: React.FC<ConversationHeaderProps> = ({\n  selectedModel,\n  setSelectedModel,\n  models,\n  showAvatar,\n  selectedKnowledgeBase,\n  setSelectedKnowledgeBase,\n  knowledgeBases,\n}) => (\n  <div className=\"p-0 flex flex-col sm:flex-row items-start sm:items-center justify-between pb-2 animate-fade-in\">\n    <div className=\"flex items-center space-x-4 mb-2 sm:mb-0\">\n      {showAvatar && (\n        <>\n          <Avatar className=\"w-10 h-10 border\">\n            <AvatarImage\n              src=\"/ant-logo.svg\"\n              alt=\"AI Assistant Avatar\"\n              width={40}\n              height={40}\n            />\n            <AvatarFallback>AI</AvatarFallback>\n          </Avatar>\n          <div>\n            <h3 className=\"text-sm font-medium leading-none\">AI Agent</h3>\n            <p className=\"text-sm text-muted-foreground\">Customer support</p>\n          </div>\n        </>\n      )}\n    </div>\n    <div className=\"flex space-x-2 w-full sm:w-auto\">\n      <DropdownMenu>\n        <DropdownMenuTrigger asChild>\n          <Button\n            variant=\"outline\"\n            size=\"sm\"\n            className=\"flex-grow text-muted-foreground sm:flex-grow-0\"\n          >\n            {models.find((m) => m.id === selectedModel)?.name}\n            <ChevronDown className=\"ml-2 h-4 w-4\" />\n          </Button>\n        </DropdownMenuTrigger>\n        <DropdownMenuContent>\n          {models.map((model) => (\n            <DropdownMenuItem\n              key={model.id}\n              onSelect={() => setSelectedModel(model.id)}\n            >\n              {model.name}\n            </DropdownMenuItem>\n          ))}\n        </DropdownMenuContent>\n      </DropdownMenu>\n      <DropdownMenu>\n        <DropdownMenuTrigger asChild>\n          <Button\n            variant=\"outline\"\n            size=\"sm\"\n            className=\"flex-grow text-muted-foreground  sm:flex-grow-0\"\n          >\n            {knowledgeBases.find((kb) => kb.id === selectedKnowledgeBase)\n              ?.name || \"Select KB\"}\n            <ChevronDown className=\"ml-2 h-4 w-4\" />\n          </Button>\n        </DropdownMenuTrigger>\n        <DropdownMenuContent>\n          {knowledgeBases.map((kb) => (\n            <DropdownMenuItem\n              key={kb.id}\n              onSelect={() => setSelectedKnowledgeBase(kb.id)}\n            >\n              {kb.name}\n            </DropdownMenuItem>\n          ))}\n        </DropdownMenuContent>\n      </DropdownMenu>\n    </div>\n  </div>\n);\n\nfunction ChatArea() {\n  const [messages, setMessages] = useState<Message[]>([]);\n  const [input, setInput] = useState(\"\");\n  const [isLoading, setIsLoading] = useState(false);\n  const [showHeader, setShowHeader] = useState(false);\n  const [selectedModel, setSelectedModel] = useState(\"claude-haiku-4-5-20251001\");\n  const [showAvatar, setShowAvatar] = useState(false);\n\n  const messagesEndRef = useRef<HTMLDivElement>(null);\n  const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState(\n    \"your-knowledge-base-id\",\n  );\n\n  const knowledgeBases: KnowledgeBase[] = [\n    { id: \"your-knowledge-base-id\", name: \"Your KB Name\" },\n    // Add more knowledge bases as needed\n  ];\n\n  const models: Model[] = [\n    { id: \"claude-3-haiku-20240307\", name: \"Claude 3 Haiku\" },\n    { id: \"claude-haiku-4-5-20251001\", name: \"Claude 4.5 Haiku\" },\n    { id: \"claude-3-5-sonnet-20240620\", name: \"Claude 3.5 Sonnet\" },\n  ];\n\n  const scrollToBottom = () => {\n    messagesEndRef.current?.scrollIntoView({ behavior: \"smooth\" });\n  };\n\n  useEffect(() => {\n    console.log(\"🔍 Messages changed! Count:\", messages.length);\n\n    const scrollToNewestMessage = () => {\n      if (messagesEndRef.current) {\n        console.log(\"📜 Scrolling to newest message...\");\n        const behavior = messages.length <= 2 ? \"auto\" : \"smooth\";\n        messagesEndRef.current.scrollIntoView({ behavior, block: \"end\" });\n      } else {\n        console.log(\"❌ No scroll anchor found!\");\n      }\n    };\n\n    if (messages.length > 0) {\n      setTimeout(scrollToNewestMessage, 100);\n    }\n  }, [messages]);\n\n  useEffect(() => {\n    if (!config.includeLeftSidebar) {\n      // If LeftSidebar is not included, we need to handle the 'updateSidebar' event differently\n      const handleUpdateSidebar = (event: CustomEvent<ThinkingContent>) => {\n        console.log(\"LeftSidebar not included. Event data:\", event.detail);\n        // You might want to handle this data differently when LeftSidebar is not present\n      };\n\n      window.addEventListener(\n        \"updateSidebar\" as any,\n        handleUpdateSidebar as EventListener,\n      );\n      return () =>\n        window.removeEventListener(\n          \"updateSidebar\" as any,\n          handleUpdateSidebar as EventListener,\n        );\n    }\n  }, []);\n\n  useEffect(() => {\n    if (!config.includeRightSidebar) {\n      // If RightSidebar is not included, we need to handle the 'updateRagSources' event differently\n      const handleUpdateRagSources = (event: CustomEvent) => {\n        console.log(\"RightSidebar not included. RAG sources:\", event.detail);\n        // You might want to handle this data differently when RightSidebar is not present\n      };\n\n      window.addEventListener(\n        \"updateRagSources\" as any,\n        handleUpdateRagSources as EventListener,\n      );\n      return () =>\n        window.removeEventListener(\n          \"updateRagSources\" as any,\n          handleUpdateRagSources as EventListener,\n        );\n    }\n  }, []);\n\n  const decodeDebugData = (response: Response) => {\n    const debugData = response.headers.get(\"X-Debug-Data\");\n    if (debugData) {\n      try {\n        const parsed = JSON.parse(debugData);\n        console.log(\"🔍 Server Debug:\", parsed.msg, parsed.data);\n      } catch (e) {\n        console.error(\"Debug decode failed:\", e);\n      }\n    }\n  };\n\n  const logDuration = (label: string, duration: number) => {\n    console.log(`⏱️ ${label}: ${duration.toFixed(2)}ms`);\n  };\n\n  const handleSubmit = async (\n    event: React.FormEvent<HTMLFormElement> | string,\n  ) => {\n    if (typeof event !== \"string\") {\n      event.preventDefault();\n    }\n    if (!showHeader) setShowHeader(true);\n    if (!showAvatar) setShowAvatar(true);\n    setIsLoading(true);\n\n    const clientStart = performance.now();\n    console.log(\"🔄 Starting request: \" + new Date().toISOString());\n\n    const userMessage = {\n      id: crypto.randomUUID(),\n      role: \"user\",\n      content: typeof event === \"string\" ? event : input,\n    };\n\n    const placeholderMessage = {\n      id: crypto.randomUUID(),\n      role: \"assistant\",\n      content: JSON.stringify({\n        response: \"\",\n        thinking: \"AI is processing...\",\n        user_mood: \"neutral\",\n        debug: {\n          context_used: false,\n        },\n      }),\n    };\n\n    setMessages((prevMessages) => [\n      ...prevMessages,\n      userMessage,\n      placeholderMessage,\n    ]);\n    setInput(\"\");\n\n    const placeholderDisplayed = performance.now();\n    logDuration(\"Perceived Latency\", placeholderDisplayed - clientStart);\n\n    try {\n      console.log(\"➡️ Sending message to API:\", userMessage.content);\n      const startTime = performance.now();\n      const response = await fetch(\"/api/chat\", {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({\n          messages: [...messages, userMessage],\n          model: selectedModel,\n          knowledgeBaseId: selectedKnowledgeBase,\n        }),\n      });\n\n      const responseReceived = performance.now();\n      logDuration(\"Full Round Trip\", responseReceived - startTime);\n      logDuration(\"Network Duration\", responseReceived - startTime);\n\n      decodeDebugData(response);\n\n      if (!response.ok) {\n        throw new Error(`API request failed with status ${response.status}`);\n      }\n\n      const data = await response.json();\n      const endTime = performance.now();\n      logDuration(\"JSON Parse Duration\", endTime - responseReceived);\n      logDuration(\"Total API Duration\", endTime - startTime);\n      console.log(\"⬅️ Received response from API:\", data);\n\n      const suggestedQuestionsHeader = response.headers.get(\n        \"x-suggested-questions\",\n      );\n      if (suggestedQuestionsHeader) {\n        data.suggested_questions = JSON.parse(suggestedQuestionsHeader);\n      }\n\n      const ragHeader = response.headers.get(\"x-rag-sources\");\n      if (ragHeader) {\n        const ragProcessed = performance.now();\n        logDuration(\n          \"🔍 RAG Processing Duration\",\n          ragProcessed - responseReceived,\n        );\n        const sources = JSON.parse(ragHeader);\n        window.dispatchEvent(\n          new CustomEvent(\"updateRagSources\", {\n            detail: {\n              sources,\n              query: userMessage.content,\n              debug: data.debug,\n            },\n          }),\n        );\n      }\n\n      const readyToRender = performance.now();\n      logDuration(\"Response Processing\", readyToRender - responseReceived);\n\n      setMessages((prevMessages) => {\n        const newMessages = [...prevMessages];\n        const lastIndex = newMessages.length - 1;\n        newMessages[lastIndex] = {\n          id: crypto.randomUUID(),\n          role: \"assistant\",\n          content: JSON.stringify(data),\n        };\n        return newMessages;\n      });\n\n      const sidebarEvent = new CustomEvent(\"updateSidebar\", {\n        detail: {\n          id: data.id,\n          content: data.thinking?.trim(),\n          user_mood: data.user_mood,\n          debug: data.debug,\n          matched_categories: data.matched_categories,\n        },\n      });\n      window.dispatchEvent(sidebarEvent);\n\n      if (data.redirect_to_agent && data.redirect_to_agent.should_redirect) {\n        window.dispatchEvent(\n          new CustomEvent(\"agentRedirectRequested\", {\n            detail: data.redirect_to_agent,\n          }),\n        );\n      }\n    } catch (error) {\n      console.error(\"Error fetching chat response:\", error);\n      console.error(\"Failed to process message:\", userMessage.content);\n    } finally {\n      setIsLoading(false);\n      const clientEnd = performance.now();\n      logDuration(\"Total Client Operation\", clientEnd - clientStart);\n    }\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n    if (e.key === \"Enter\" && !e.shiftKey) {\n      e.preventDefault();\n      if (input.trim() !== \"\") {\n        handleSubmit(e as any);\n      }\n    }\n  };\n\n  const handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {\n    const textarea = event.target;\n    setInput(textarea.value);\n\n    textarea.style.height = \"auto\";\n    textarea.style.height = `${Math.min(textarea.scrollHeight, 300)}px`;\n  };\n\n  const handleSuggestedQuestionClick = (question: string) => {\n    handleSubmit(question);\n  };\n\n  useEffect(() => {\n    const handleToolExecution = (event: Event) => {\n      const customEvent = event as CustomEvent<{\n        ui: { type: string; props: any };\n      }>;\n      console.log(\"Tool execution event received:\", customEvent.detail);\n    };\n\n    window.addEventListener(\"toolExecution\", handleToolExecution);\n    return () =>\n      window.removeEventListener(\"toolExecution\", handleToolExecution);\n  }, []);\n\n  return (\n    <Card className=\"flex-1 flex flex-col mb-4 mr-4 ml-4\">\n      <CardContent className=\"flex-1 flex flex-col overflow-hidden pt-4 px-4 pb-0\">\n        <ConversationHeader\n          selectedModel={selectedModel}\n          setSelectedModel={setSelectedModel}\n          models={models}\n          showAvatar={showAvatar}\n          selectedKnowledgeBase={selectedKnowledgeBase}\n          setSelectedKnowledgeBase={setSelectedKnowledgeBase}\n          knowledgeBases={knowledgeBases}\n        />\n        <div className=\"flex-1 overflow-y-auto p-4 space-y-4\">\n          {messages.length === 0 ? (\n            <div className=\"flex flex-col items-center justify-center h-full animate-fade-in-up\">\n              <Avatar className=\"w-10 h-10 mb-4 border\">\n                <AvatarImage\n                  src=\"/ant-logo.svg\"\n                  alt=\"AI Assistant Avatar\"\n                  width={40}\n                  height={40}\n                />\n              </Avatar>\n              <h2 className=\"text-2xl font-semibold mb-8\">\n                Here&apos;s how I can help\n              </h2>\n              <div className=\"space-y-4 text-sm\">\n                <div className=\"flex items-center gap-3\">\n                  <HandHelping className=\"text-muted-foreground\" />\n                  <p className=\"text-muted-foreground\">\n                    Need guidance? I&apos;ll help navigate tasks using internal\n                    resources.\n                  </p>\n                </div>\n                <div className=\"flex items-center gap-3\">\n                  <WandSparkles className=\"text-muted-foreground\" />\n                  <p className=\"text-muted-foreground\">\n                    I&apos;m a whiz at finding information! I can dig through\n                    your knowledge base.\n                  </p>\n                </div>\n                <div className=\"flex items-center gap-3\">\n                  <BookOpenText className=\"text-muted-foreground\" />\n                  <p className=\"text-muted-foreground\">\n                    I&apos;m always learning! The more you share, the better I\n                    can assist you.\n                  </p>\n                </div>\n              </div>\n            </div>\n          ) : (\n            <div className=\"space-y-4\">\n              {messages.map((message, index) => (\n                <div key={message.id}>\n                  <div\n                    className={`flex items-start ${\n                      message.role === \"user\" ? \"justify-end\" : \"\"\n                    } ${\n                      index === messages.length - 1 ? \"animate-fade-in-up\" : \"\"\n                    }`}\n                    style={{\n                      animationDuration: \"300ms\",\n                      animationFillMode: \"backwards\",\n                    }}\n                  >\n                    {message.role === \"assistant\" && (\n                      <Avatar className=\"w-8 h-8 mr-2 border\">\n                        <AvatarImage\n                          src=\"/ant-logo.svg\"\n                          alt=\"AI Assistant Avatar\"\n                        />\n                        <AvatarFallback>AI</AvatarFallback>\n                      </Avatar>\n                    )}\n                    <div\n                      className={`p-3 rounded-md text-sm max-w-[65%] ${\n                        message.role === \"user\"\n                          ? \"bg-primary text-primary-foreground\"\n                          : \"bg-muted border\"\n                      }`}\n                    >\n                      <MessageContent\n                        content={message.content}\n                        role={message.role}\n                      />\n                    </div>\n                  </div>\n                  {message.role === \"assistant\" && (\n                    <SuggestedQuestions\n                      questions={\n                        JSON.parse(message.content).suggested_questions || []\n                      }\n                      onQuestionClick={handleSuggestedQuestionClick}\n                      isLoading={isLoading}\n                    />\n                  )}\n                </div>\n              ))}\n              <div ref={messagesEndRef} style={{ height: \"1px\" }} />\n            </div>\n          )}\n        </div>\n      </CardContent>\n\n      <CardFooter className=\"p-4 pt-0\">\n        <form\n          onSubmit={handleSubmit}\n          className=\"flex flex-col w-full relative bg-background border rounded-xl focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2\"\n        >\n          <Textarea\n            value={input}\n            onChange={handleInputChange}\n            onKeyDown={handleKeyDown}\n            placeholder=\"Type your message here...\"\n            disabled={isLoading}\n            className=\"resize-none min-h-[44px] bg-background  border-0 p-3 rounded-xl shadow-none focus-visible:ring-0\"\n            rows={1}\n          />\n          <div className=\"flex justify-between items-center p-3\">\n            <div>\n              <Image\n                src=\"/claude-icon.svg\"\n                alt=\"Claude Icon\"\n                width={0}\n                height={14}\n                className=\"w-auto h-[14px] mt-1\"\n              />\n            </div>\n            <Button\n              type=\"submit\"\n              disabled={isLoading || input.trim() === \"\"}\n              className=\"gap-2\"\n              size=\"sm\"\n            >\n              {isLoading ? (\n                <div className=\"animate-spin h-5 w-5 border-t-2 border-white rounded-full\" />\n              ) : (\n                <>\n                  Send Message\n                  <Send className=\"h-4 w-4\" />\n                </>\n              )}\n            </Button>\n          </div>\n        </form>\n      </CardFooter>\n    </Card>\n  );\n}\n\nexport default ChatArea;\n"
  },
  {
    "path": "customer-support-agent/components/FullSourceModal.tsx",
    "content": "import React from \"react\";\nimport ReactMarkdown from \"react-markdown\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { Button } from \"@/components/ui/button\";\nimport { RAGSource } from \"@/app/lib/utils\";\n\ninterface FullSourceModalProps {\n  isOpen: boolean;\n  onClose: () => void;\n  source: RAGSource | null;\n}\n\nconst FullSourceModal: React.FC<FullSourceModalProps> = ({\n  isOpen,\n  onClose,\n  source,\n}) => {\n  return (\n    <Dialog open={isOpen} onOpenChange={onClose}>\n      <DialogContent className=\"max-w-[600px] max-h-[80vh] overflow-y-auto\">\n        <DialogHeader>\n          <DialogTitle>{source?.fileName || \"Full Source\"}</DialogTitle>\n        </DialogHeader>\n        {source ? (\n          <>\n            <div className=\"mb-4\">\n              <span className=\"font-bold\">Score: </span>\n              {(source.score * 100).toFixed(2)}%\n            </div>\n            <ReactMarkdown className=\"max-h-[60vh] overflow-y-auto mb-4\">\n              {source.snippet}\n            </ReactMarkdown>\n          </>\n        ) : (\n          <p>No source selected</p>\n        )}\n        <Button onClick={onClose}>Close</Button>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nexport default FullSourceModal;\n"
  },
  {
    "path": "customer-support-agent/components/LeftSidebar.tsx",
    "content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { Card, CardHeader, CardTitle, CardContent } from \"@/components/ui/card\";\nimport {\n  User,\n  DollarSign,\n  Info,\n  Wrench,\n  Zap,\n  Building2,\n  Scale,\n  ChartBarBig,\n  CircleHelp,\n} from \"lucide-react\";\n\ninterface ThinkingContent {\n  id: string;\n  content: string;\n  user_mood?: string;\n  matched_categories?: string[];\n  debug?: {\n    context_used: boolean;\n  };\n}\n\nconst getDebugPillColor = (value: boolean): string => {\n  return value\n    ? \"bg-green-100 text-green-800 border-green-300\" // Success\n    : \"bg-yellow-100 text-yellow-800 border-yellow-300\"; // Not Used/Not Relevant\n};\n\nconst getMoodColor = (mood: string): string => {\n  const colors: { [key: string]: string } = {\n    positive: \"bg-green-100 text-green-800\",\n    negative: \"bg-red-100 text-red-800\",\n    curious: \"bg-blue-100 text-blue-800\",\n    frustrated: \"bg-orange-100 text-orange-800\",\n    confused: \"bg-yellow-100 text-yellow-800\",\n    neutral: \"bg-gray-100 text-gray-800\",\n  };\n  return colors[mood?.toLowerCase()] || \"bg-gray-100 text-gray-800\";\n};\n\nconst MAX_THINKING_HISTORY = 15;\n\nconst LeftSidebar: React.FC = () => {\n  const [thinkingContents, setThinkingContents] = useState<ThinkingContent[]>(\n    [],\n  );\n\n  useEffect(() => {\n    const handleUpdateSidebar = (event: CustomEvent<ThinkingContent>) => {\n      if (event.detail && event.detail.id) {\n        console.log(\"🔍 DEBUG: Sidebar Event:\", event.detail);\n        setThinkingContents((prev) => {\n          const exists = prev.some((item) => item.id === event.detail.id);\n          if (!exists) {\n            console.log(\n              \"📝 New thinking entry: \",\n              event.detail.content.slice(0, 50) + \"...\",\n            ); // Shows first 50 chars\n\n            // Add a timestamp!\n            const enhancedEntry = {\n              ...event.detail,\n              timestamp: new Date().toISOString(),\n            };\n\n            const newHistory = [enhancedEntry, ...prev].slice(\n              0,\n              MAX_THINKING_HISTORY,\n            ); // Always keep latest 20\n\n            return newHistory;\n          }\n          return prev;\n        });\n      } else {\n        console.warn(\"Missing 'id' in sidebar event detail:\", event.detail);\n      }\n    };\n\n    window.addEventListener(\n      \"updateSidebar\",\n      handleUpdateSidebar as EventListener,\n    );\n    return () =>\n      window.removeEventListener(\n        \"updateSidebar\",\n        handleUpdateSidebar as EventListener,\n      );\n  }, []);\n\n  return (\n    <aside className=\"w-[380px] pl-4 overflow-hidden pb-4\">\n      <Card className=\"h-full overflow-hidden\">\n        <CardHeader>\n          <CardTitle className=\"text-sm font-medium leading-none\">\n            Assistant Thinking\n          </CardTitle>\n        </CardHeader>\n        <CardContent className=\"overflow-y-auto h-[calc(100%-45px)]\">\n          {thinkingContents.length === 0 ? (\n            <div className=\"text-sm text-muted-foreground\">\n              The assistant inner dialogue will appear here for you to debug it\n            </div>\n          ) : (\n            thinkingContents.map((content) => (\n              <Card\n                key={content.id}\n                className=\"mb-4 animate-fade-in-up\"\n                style={{\n                  animationDuration: \"600ms\",\n                  animationFillMode: \"backwards\",\n                  animationTimingFunction: \"cubic-bezier(0.2, 0.8, 0.2, 1)\", // This adds bounce\n                }}\n              >\n                <CardContent className=\"py-4\">\n                  <div className=\"text-sm text-muted-foreground\">\n                    {content.content}\n                  </div>\n                  {content.user_mood && content.debug && (\n                    <div className=\"flex items-center space-x-2 mt-4 text-xs\">\n                      {/* Mood */}\n                      <span\n                        className={`px-2 py-1 rounded-full ${getMoodColor(content.user_mood)}`}\n                      >\n                        {content.user_mood.charAt(0).toUpperCase() +\n                          content.user_mood.slice(1)}\n                      </span>\n\n                      <span\n                        className={`px-2 py-1 rounded-full ${getDebugPillColor(content.debug.context_used)}`}\n                      >\n                        Context: {content.debug.context_used ? \"✅\" : \"❌\"}\n                      </span>\n                    </div>\n                  )}\n                  {content.matched_categories &&\n                    content.matched_categories.length > 0 && (\n                      <div className=\"mt-2\">\n                        {content.matched_categories.map((category) => (\n                          <div\n                            key={category}\n                            className=\"inline-flex items-center mr-2 mt-2 text-muted-foreground text-xs py-0\"\n                          >\n                            {category === \"account\" && (\n                              <User className=\"w-3 h-3 mr-1\" />\n                            )}\n                            {category === \"billing\" && (\n                              <DollarSign className=\"w-3 h-3 mr-1\" />\n                            )}\n                            {category === \"feature\" && (\n                              <Zap className=\"w-3 h-3 mr-1\" />\n                            )}\n                            {category === \"internal\" && (\n                              <Building2 className=\"w-3 h-3 mr-1\" />\n                            )}\n                            {category === \"legal\" && (\n                              <Scale className=\"w-3 h-3 mr-1\" />\n                            )}\n                            {category === \"other\" && (\n                              <CircleHelp className=\"w-3 h-3 mr-1\" />\n                            )}\n                            {category === \"technical\" && (\n                              <Wrench className=\"w-3 h-3 mr-1\" />\n                            )}\n                            {category === \"usage\" && (\n                              <ChartBarBig className=\"w-3 h-3 mr-1\" />\n                            )}\n                            {category\n                              .split(\"_\")\n                              .map(\n                                (word) =>\n                                  word.charAt(0).toUpperCase() + word.slice(1),\n                              )\n                              .join(\" \")}\n                          </div>\n                        ))}\n                      </div>\n                    )}\n                </CardContent>\n              </Card>\n            ))\n          )}\n        </CardContent>\n      </Card>\n    </aside>\n  );\n};\n\nexport default LeftSidebar;\n"
  },
  {
    "path": "customer-support-agent/components/RightSidebar.tsx",
    "content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { Card, CardHeader, CardTitle, CardContent } from \"@/components/ui/card\";\nimport { FileIcon, MessageCircleIcon } from \"lucide-react\";\nimport FullSourceModal from \"./FullSourceModal\";\n\ninterface RAGSource {\n  id: string;\n  fileName: string;\n  snippet: string;\n  score: number;\n  timestamp?: string;\n}\n\ninterface RAGHistoryItem {\n  sources: RAGSource[];\n  timestamp: string;\n  query: string;\n}\n\ninterface DebugInfo {\n  context_used: boolean;\n}\n\ninterface SidebarEvent {\n  id: string;\n  content: string;\n  user_mood?: string;\n  debug?: DebugInfo;\n}\n\nconst truncateSnippet = (text: string): string => {\n  return text?.length > 150 ? `${text.slice(0, 100)}...` : text || \"\";\n};\n\nconst getScoreColor = (score: number): string => {\n  if (score > 0.6) return \"bg-green-100 text-green-800\";\n  if (score > 0.4) return \"bg-yellow-100 text-yellow-800\";\n  return \"bg-red-100 text-red-800\";\n};\n\nconst MAX_HISTORY = 15;\n\nconst RightSidebar: React.FC = () => {\n  const [ragHistory, setRagHistory] = useState<RAGHistoryItem[]>([]);\n  const [shouldShowSources, setShouldShowSources] = useState(false);\n  const [isModalOpen, setIsModalOpen] = useState(false);\n  const [selectedSource, setSelectedSource] = useState<RAGSource | null>(null);\n\n  useEffect(() => {\n    const updateRAGSources = (\n      event: CustomEvent<{\n        sources: RAGSource[];\n        query: string;\n        debug?: DebugInfo;\n      }>,\n    ) => {\n      console.log(\"🔍 RAG event received:\", event.detail);\n      const { sources, query, debug } = event.detail;\n\n      const shouldDisplaySources = debug?.context_used;\n\n      if (\n        Array.isArray(sources) &&\n        sources.length > 0 &&\n        shouldDisplaySources\n      ) {\n        const cleanedSources = sources.map((source) => ({\n          ...source,\n          snippet: source.snippet || \"No preview available\",\n          fileName:\n            (source.fileName || \"\").replace(/_/g, \" \").replace(\".txt\", \"\") ||\n            \"Unnamed\",\n          timestamp: new Date().toISOString(),\n        }));\n\n        const historyItem: RAGHistoryItem = {\n          sources: cleanedSources,\n          timestamp: new Date().toISOString(),\n          query: query || \"Unknown query\",\n        };\n\n        setRagHistory((prev) => {\n          const newHistory = [historyItem, ...prev];\n          return newHistory.slice(0, MAX_HISTORY);\n        });\n\n        console.log(\n          \"🔍 Sources displayed:\",\n          shouldDisplaySources ? \"YES\" : \"NO\",\n        );\n      }\n    };\n\n    const updateDebug = (event: CustomEvent<SidebarEvent>) => {\n      const debug = event.detail.debug;\n      const shouldShow = debug?.context_used ?? false;\n      setShouldShowSources(shouldShow);\n    };\n\n    window.addEventListener(\n      \"updateRagSources\" as any,\n      updateRAGSources as EventListener,\n    );\n    window.addEventListener(\n      \"updateSidebar\" as any,\n      updateDebug as EventListener,\n    );\n\n    return () => {\n      window.removeEventListener(\n        \"updateRagSources\" as any,\n        updateRAGSources as EventListener,\n      );\n      window.removeEventListener(\n        \"updateSidebar\" as any,\n        updateDebug as EventListener,\n      );\n    };\n  }, []);\n\n  const handleViewFullSource = (source: RAGSource) => {\n    setSelectedSource(source);\n    setIsModalOpen(true);\n  };\n\n  const fadeInUpClass = \"animate-fade-in-up\";\n  const fadeStyle = {\n    animationDuration: \"600ms\",\n    animationFillMode: \"backwards\",\n    animationTimingFunction: \"cubic-bezier(0.2, 0.8, 0.2, 1)\",\n  };\n\n  return (\n    <aside className=\"w-[380px] pr-4 overflow-hidden pb-4\">\n      <Card\n        className={`${fadeInUpClass} h-full overflow-hidden`}\n        style={fadeStyle}\n      >\n        <CardHeader>\n          <CardTitle className=\"text-sm font-medium leading-none\">\n            Knowledge Base History\n          </CardTitle>\n        </CardHeader>\n        <CardContent className=\"overflow-y-auto h-[calc(100%-45px)]\">\n          {ragHistory.length === 0 && (\n            <div className=\"text-sm text-muted-foreground\">\n              The assistant will display sources here once finding them\n            </div>\n          )}\n          {ragHistory.map((historyItem, index) => (\n            <div\n              key={historyItem.timestamp}\n              className={`mb-6 ${fadeInUpClass}`}\n              style={{ ...fadeStyle, animationDelay: `${index * 50}ms` }}\n            >\n              <div className=\"flex items-center text-xs text-muted-foreground mb-2 gap-1\">\n                <MessageCircleIcon\n                  size={14}\n                  className=\"text-muted-foreground\"\n                />\n                <span>{historyItem.query}</span>\n              </div>\n              {historyItem.sources.map((source, sourceIndex) => (\n                <Card\n                  key={source.id}\n                  className={`mb-2 ${fadeInUpClass}`}\n                  style={{\n                    ...fadeStyle,\n                    animationDelay: `${index * 100 + sourceIndex * 75}ms`,\n                  }}\n                >\n                  <CardContent className=\"py-4\">\n                    <p className=\"text-sm text-muted-foreground\">\n                      {truncateSnippet(source.snippet)}\n                    </p>\n                    <div className=\"flex flex-col gap-2\">\n                      <div\n                        className={`${getScoreColor(source.score)} px-2 py-1 mt-4 rounded-full text-xs inline-block w-fit`}\n                      >\n                        {(source.score * 100).toFixed(0)}% match\n                      </div>\n                      <div\n                        className=\"inline-flex items-center mr-2 mt-2 text-muted-foreground text-xs py-0 cursor-pointer hover:text-gray-600\"\n                        onClick={() => handleViewFullSource(source)}\n                      >\n                        <FileIcon className=\"w-4 h-4 min-w-[12px] min-h-[12px] mr-2\" />\n                        <span className=\"text-xs underline\">\n                          {truncateSnippet(source.fileName || \"Unnamed\")}\n                        </span>\n                      </div>\n                    </div>\n                  </CardContent>\n                </Card>\n              ))}\n            </div>\n          ))}\n        </CardContent>\n      </Card>\n      <FullSourceModal\n        isOpen={isModalOpen}\n        onClose={() => setIsModalOpen(false)}\n        source={selectedSource}\n      />\n    </aside>\n  );\n};\n\nexport default RightSidebar;\n"
  },
  {
    "path": "customer-support-agent/components/TopNavBar.tsx",
    "content": "\"use client\";\nimport React, { useState, useEffect } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport Link from \"next/link\";\nimport Image from \"next/image\";\nimport { Moon, Sun, Check } from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\nimport { themes } from \"@/styles/themes\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\n\nconst themeColors = {\n  neutral: \"#000000\",\n  red: \"#EF4444\",\n  violet: \"#8B5CF6\",\n  blue: \"#3B82F6\",\n  tangerine: \"#F97316\",\n  emerald: \"#10B981\",\n  amber: \"#F59E0B\",\n} as const;\n\ntype ThemeName = keyof typeof themes;\n\nconst ColorCircle = ({\n  themeName,\n  isSelected,\n}: {\n  themeName: ThemeName;\n  isSelected: boolean;\n}) => (\n  <div\n    className=\"relative border flex h-4 w-4 shrink-0 items-center justify-center rounded-full\"\n    style={{ backgroundColor: themeColors[themeName] }}\n  >\n    {isSelected && (\n      <div className=\"absolute inset-0 flex items-center justify-center\">\n        <Check className=\"text-white\" size={12} />\n      </div>\n    )}\n  </div>\n);\n\nconst TopNavBar = () => {\n  const { theme, setTheme } = useTheme();\n  const [colorTheme, setColorTheme] = useState<ThemeName>(\"neutral\");\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n    const savedColorTheme = (localStorage.getItem(\"color-theme\") ||\n      \"neutral\") as ThemeName;\n    setColorTheme(savedColorTheme);\n    applyTheme(savedColorTheme, theme === \"dark\");\n  }, [theme]);\n\n  const applyTheme = (newColorTheme: ThemeName, isDark: boolean) => {\n    const root = document.documentElement;\n    const themeVariables = isDark\n      ? themes[newColorTheme].dark\n      : themes[newColorTheme].light;\n\n    Object.entries(themeVariables).forEach(([key, value]) => {\n      root.style.setProperty(`--${key}`, value as string);\n    });\n  };\n\n  const handleThemeChange = (newColorTheme: ThemeName) => {\n    setColorTheme(newColorTheme);\n    localStorage.setItem(\"color-theme\", newColorTheme);\n    applyTheme(newColorTheme, theme === \"dark\");\n  };\n\n  const handleModeChange = (mode: \"light\" | \"dark\" | \"system\") => {\n    setTheme(mode);\n    if (mode !== \"system\") {\n      applyTheme(colorTheme, mode === \"dark\");\n    }\n  };\n\n  if (!mounted) {\n    return null;\n  }\n\n  return (\n    <nav className=\"text-foreground p-4 flex justify-between items-center\">\n      <div className=\"font-bold text-xl flex gap-2 items-center\">\n        <Image\n          src={theme === \"dark\" ? \"/wordmark-dark.svg\" : \"/wordmark.svg\"}\n          alt=\"Company Wordmark\"\n          width={112}\n          height={20}\n        />\n      </div>\n      <div className=\"flex items-center gap-2\">\n        <DropdownMenu>\n          <DropdownMenuTrigger asChild>\n            <Button variant=\"outline\">\n              <ColorCircle themeName={colorTheme} isSelected={false} />\n            </Button>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent align=\"end\">\n            {(Object.keys(themes) as ThemeName[]).map((themeName) => (\n              <DropdownMenuItem\n                key={themeName}\n                onClick={() => handleThemeChange(themeName)}\n                className=\"flex items-center gap-2\"\n              >\n                <ColorCircle\n                  themeName={themeName}\n                  isSelected={colorTheme === themeName}\n                />\n                {themeName.charAt(0).toUpperCase() + themeName.slice(1)}\n              </DropdownMenuItem>\n            ))}\n          </DropdownMenuContent>\n        </DropdownMenu>\n        <DropdownMenu>\n          <DropdownMenuTrigger asChild>\n            <Button variant=\"outline\" size=\"icon\">\n              <Sun className=\"h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0\" />\n              <Moon className=\"absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100\" />\n              <span className=\"sr-only\">Toggle theme</span>\n            </Button>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent align=\"end\">\n            <DropdownMenuItem onClick={() => handleModeChange(\"light\")}>\n              Light\n            </DropdownMenuItem>\n            <DropdownMenuItem onClick={() => handleModeChange(\"dark\")}>\n              Dark\n            </DropdownMenuItem>\n            <DropdownMenuItem onClick={() => handleModeChange(\"system\")}>\n              System\n            </DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n        <Link\n          href=\"https://github.com/anthropics/anthropic-quickstarts\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n        >\n          <Button variant=\"outline\" className=\"text-foreground\">\n            Deploy your own\n          </Button>\n        </Link>\n      </div>\n    </nav>\n  );\n};\n\nexport default TopNavBar;\n"
  },
  {
    "path": "customer-support-agent/components/theme-provider.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ThemeProvider as NextThemesProvider } from \"next-themes\";\nimport { type ThemeProviderProps } from \"next-themes/dist/types\";\n\nexport function ThemeProvider({ children, ...props }: ThemeProviderProps) {\n  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;\n}\n"
  },
  {
    "path": "customer-support-agent/components/ui/avatar.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\"\n\nimport { cn } from \"@/app/lib/utils\"\n\nconst Avatar = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full\",\n      className\n    )}\n    {...props}\n  />\n))\nAvatar.displayName = AvatarPrimitive.Root.displayName\n\nconst AvatarImage = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Image>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Image\n    ref={ref}\n    className={cn(\"aspect-square h-full w-full\", className)}\n    {...props}\n  />\n))\nAvatarImage.displayName = AvatarPrimitive.Image.displayName\n\nconst AvatarFallback = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Fallback>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Fallback\n    ref={ref}\n    className={cn(\n      \"flex h-full w-full items-center justify-center rounded-full bg-muted\",\n      className\n    )}\n    {...props}\n  />\n))\nAvatarFallback.displayName = AvatarPrimitive.Fallback.displayName\n\nexport { Avatar, AvatarImage, AvatarFallback }\n"
  },
  {
    "path": "customer-support-agent/components/ui/button.tsx",
    "content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/app/lib/utils\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n        outline:\n          \"border border-input bg-background hover:bg-accent hover:text-accent-foreground\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-10 px-4 py-2\",\n        sm: \"h-9 rounded-md px-3\",\n        lg: \"h-11 rounded-md px-8\",\n        icon: \"h-10 w-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nexport interface ButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : \"button\";\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "customer-support-agent/components/ui/card.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/app/lib/utils\";\n\nconst Card = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\n      \"rounded-lg border bg-card text-card-foreground shadow-sm\",\n      className,\n    )}\n    {...props}\n  />\n));\nCard.displayName = \"Card\";\n\nconst CardHeader = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex flex-col space-y-1.5 p-4\", className)}\n    {...props}\n  />\n));\nCardHeader.displayName = \"CardHeader\";\n\nconst CardTitle = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n  <h3\n    ref={ref}\n    className={cn(\n      \"text-2xl font-semibold leading-none tracking-tight\",\n      className,\n    )}\n    {...props}\n  />\n));\nCardTitle.displayName = \"CardTitle\";\n\nconst CardDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n  <p\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n));\nCardDescription.displayName = \"CardDescription\";\n\nconst CardContent = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div ref={ref} className={cn(\"p-4 pt-0\", className)} {...props} />\n));\nCardContent.displayName = \"CardContent\";\n\nconst CardFooter = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex items-center p-6 pt-0\", className)}\n    {...props}\n  />\n));\nCardFooter.displayName = \"CardFooter\";\n\nexport {\n  Card,\n  CardHeader,\n  CardFooter,\n  CardTitle,\n  CardDescription,\n  CardContent,\n};\n"
  },
  {
    "path": "customer-support-agent/components/ui/dialog.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { X } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Dialog = DialogPrimitive.Root\n\nconst DialogTrigger = DialogPrimitive.Trigger\n\nconst DialogPortal = DialogPrimitive.Portal\n\nconst DialogClose = DialogPrimitive.Close\n\nconst DialogOverlay = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    className={cn(\n      \"fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className\n    )}\n    {...props}\n  />\n))\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName\n\nconst DialogContent = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n      <DialogPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground\">\n        <X className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Close</span>\n      </DialogPrimitive.Close>\n    </DialogPrimitive.Content>\n  </DialogPortal>\n))\nDialogContent.displayName = DialogPrimitive.Content.displayName\n\nconst DialogHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col space-y-1.5 text-center sm:text-left\",\n      className\n    )}\n    {...props}\n  />\n)\nDialogHeader.displayName = \"DialogHeader\"\n\nconst DialogFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n      className\n    )}\n    {...props}\n  />\n)\nDialogFooter.displayName = \"DialogFooter\"\n\nconst DialogTitle = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title\n    ref={ref}\n    className={cn(\n      \"text-lg font-semibold leading-none tracking-tight\",\n      className\n    )}\n    {...props}\n  />\n))\nDialogTitle.displayName = DialogPrimitive.Title.displayName\n\nconst DialogDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n))\nDialogDescription.displayName = DialogPrimitive.Description.displayName\n\nexport {\n  Dialog,\n  DialogPortal,\n  DialogOverlay,\n  DialogClose,\n  DialogTrigger,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n}\n"
  },
  {
    "path": "customer-support-agent/components/ui/dropdown-menu.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\nimport { Check, ChevronRight, Circle } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst DropdownMenu = DropdownMenuPrimitive.Root\n\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger\n\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group\n\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal\n\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub\n\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\n    inset?: boolean\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      \"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRight className=\"ml-auto h-4 w-4\" />\n  </DropdownMenuPrimitive.SubTrigger>\n))\nDropdownMenuSubTrigger.displayName =\n  DropdownMenuPrimitive.SubTrigger.displayName\n\nconst DropdownMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuSubContent.displayName =\n  DropdownMenuPrimitive.SubContent.displayName\n\nconst DropdownMenuContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <DropdownMenuPrimitive.Portal>\n    <DropdownMenuPrimitive.Content\n      ref={ref}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className\n      )}\n      {...props}\n    />\n  </DropdownMenuPrimitive.Portal>\n))\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName\n\nconst DropdownMenuItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\n    inset?: boolean\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <DropdownMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.CheckboxItem>\n))\nDropdownMenuCheckboxItem.displayName =\n  DropdownMenuPrimitive.CheckboxItem.displayName\n\nconst DropdownMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Circle className=\"h-2 w-2 fill-current\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.RadioItem>\n))\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName\n\nconst DropdownMenuLabel = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\n    inset?: boolean\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Label\n    ref={ref}\n    className={cn(\n      \"px-2 py-1.5 text-sm font-semibold\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName\n\nconst DropdownMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n))\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName\n\nconst DropdownMenuShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\"ml-auto text-xs tracking-widest opacity-60\", className)}\n      {...props}\n    />\n  )\n}\nDropdownMenuShortcut.displayName = \"DropdownMenuShortcut\"\n\nexport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuGroup,\n  DropdownMenuPortal,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuRadioGroup,\n}\n"
  },
  {
    "path": "customer-support-agent/components/ui/input.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/app/lib/utils\"\n\nexport interface InputProps\n  extends React.InputHTMLAttributes<HTMLInputElement> {}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n  ({ className, type, ...props }, ref) => {\n    return (\n      <input\n        type={type}\n        className={cn(\n          \"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n          className\n        )}\n        ref={ref}\n        {...props}\n      />\n    )\n  }\n)\nInput.displayName = \"Input\"\n\nexport { Input }\n"
  },
  {
    "path": "customer-support-agent/components/ui/textarea.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface TextareaProps\n  extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n  ({ className, ...props }, ref) => {\n    return (\n      <textarea\n        className={cn(\n          \"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n          className\n        )}\n        ref={ref}\n        {...props}\n      />\n    )\n  }\n)\nTextarea.displayName = \"Textarea\"\n\nexport { Textarea }\n"
  },
  {
    "path": "customer-support-agent/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.ts\",\n    \"css\": \"app/globals.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\"\n  }\n}"
  },
  {
    "path": "customer-support-agent/config.ts",
    "content": "type Config = {\n  includeLeftSidebar: boolean;\n  includeRightSidebar: boolean;\n};\n\nconst config: Config = {\n  includeLeftSidebar: process.env.NEXT_PUBLIC_INCLUDE_LEFT_SIDEBAR === \"true\",\n  includeRightSidebar: process.env.NEXT_PUBLIC_INCLUDE_RIGHT_SIDEBAR === \"true\",\n};\n\nexport default config;\n"
  },
  {
    "path": "customer-support-agent/lib/utils.ts",
    "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "customer-support-agent/next.config.mjs",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  experimental: {\n    serverComponentsExternalPackages: [\"@aws-sdk/client-bedrock-agent-runtime\"],\n  },\n  webpack: (config, { isServer }) => {\n    if (isServer) {\n      config.externals.push({\n        \"@aws-sdk/client-bedrock-agent-runtime\":\n          \"commonjs @aws-sdk/client-bedrock-agent-runtime\",\n      });\n    }\n    return config;\n  },\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "customer-support-agent/package.json",
    "content": "{\n  \"name\": \"assistant\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"engines\": {\n    \"node\": \">=18.17.0\"\n  },\n  \"scripts\": {\n    \"dev\": \"NEXT_PUBLIC_INCLUDE_LEFT_SIDEBAR=true NEXT_PUBLIC_INCLUDE_RIGHT_SIDEBAR=true next dev\",\n    \"build\": \"NEXT_PUBLIC_INCLUDE_LEFT_SIDEBAR=true NEXT_PUBLIC_INCLUDE_RIGHT_SIDEBAR=true next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\",\n    \"dev:full\": \"npm run dev\",\n    \"dev:left\": \"NEXT_PUBLIC_INCLUDE_LEFT_SIDEBAR=true NEXT_PUBLIC_INCLUDE_RIGHT_SIDEBAR=false next dev\",\n    \"dev:right\": \"NEXT_PUBLIC_INCLUDE_LEFT_SIDEBAR=false NEXT_PUBLIC_INCLUDE_RIGHT_SIDEBAR=true next dev\",\n    \"dev:chat\": \"NEXT_PUBLIC_INCLUDE_LEFT_SIDEBAR=false NEXT_PUBLIC_INCLUDE_RIGHT_SIDEBAR=false next dev\",\n    \"build:full\": \"npm run build\",\n    \"build:left\": \"NEXT_PUBLIC_INCLUDE_LEFT_SIDEBAR=true NEXT_PUBLIC_INCLUDE_RIGHT_SIDEBAR=false next build\",\n    \"build:right\": \"NEXT_PUBLIC_INCLUDE_LEFT_SIDEBAR=false NEXT_PUBLIC_INCLUDE_RIGHT_SIDEBAR=true next build\",\n    \"build:chat\": \"NEXT_PUBLIC_INCLUDE_LEFT_SIDEBAR=false NEXT_PUBLIC_INCLUDE_RIGHT_SIDEBAR=false next build\"\n  },\n  \"dependencies\": {\n    \"@ai-sdk/anthropic\": \"^0.0.34\",\n    \"@anthropic-ai/sdk\": \"^0.27.1\",\n    \"@aws-sdk/client-bedrock-agent-runtime\": \"^3.621.0\",\n    \"@radix-ui/react-avatar\": \"^1.1.0\",\n    \"@radix-ui/react-dialog\": \"^1.1.1\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.1.1\",\n    \"@radix-ui/react-slot\": \"^1.1.0\",\n    \"ai\": \"^3.2.38\",\n    \"class-variance-authority\": \"^0.7.0\",\n    \"clsx\": \"^2.1.1\",\n    \"lucide-react\": \"^0.417.0\",\n    \"next\": \"14.2.5\",\n    \"next-themes\": \"^0.3.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"react-markdown\": \"^9.0.1\",\n    \"rehype-highlight\": \"^7.0.0\",\n    \"rehype-raw\": \"^7.0.0\",\n    \"tailwind-merge\": \"^2.4.0\",\n    \"tailwindcss-animate\": \"^1.0.7\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^20\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"eslint\": \"^8\",\n    \"eslint-config-next\": \"14.2.5\",\n    \"postcss\": \"^8\",\n    \"tailwindcss\": \"^3.4.1\",\n    \"typescript\": \"^5\"\n  }\n}\n"
  },
  {
    "path": "customer-support-agent/postcss.config.mjs",
    "content": "/** @type {import('postcss-load-config').Config} */\nconst config = {\n  plugins: {\n    tailwindcss: {},\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "customer-support-agent/styles/themes.js",
    "content": "// themes.ts\n\nexport const themes = {\n  neutral: {\n    light: {\n      background: \"0 0% 100%\",\n      foreground: \"0 0% 3.9%\",\n      card: \"0 0% 100%\",\n      \"card-foreground\": \"0 0% 3.9%\",\n      popover: \"0 0% 100%\",\n      \"popover-foreground\": \"0 0% 3.9%\",\n      primary: \"0 0% 9%\",\n      \"primary-foreground\": \"0 0% 98%\",\n      secondary: \"0 0% 96.1%\",\n      \"secondary-foreground\": \"0 0% 9%\",\n      muted: \"0 0% 96.1%\",\n      \"muted-foreground\": \"0 0% 45.1%\",\n      accent: \"0 0% 96.1%\",\n      \"accent-foreground\": \"0 0% 9%\",\n      destructive: \"0 84.2% 60.2%\",\n      \"destructive-foreground\": \"0 0% 98%\",\n      border: \"0 0% 89.8%\",\n      input: \"0 0% 89.8%\",\n      ring: \"0 0% 3.9%\",\n      radius: \"0.75rem\",\n      \"chart-1\": \"12 76% 61%\",\n      \"chart-2\": \"173 58% 39%\",\n      \"chart-3\": \"197 37% 24%\",\n      \"chart-4\": \"43 74% 66%\",\n      \"chart-5\": \"27 87% 67%\",\n    },\n    dark: {\n      background: \"0 0% 3.9%\",\n      foreground: \"0 0% 98%\",\n      card: \"0 0% 3.9%\",\n      \"card-foreground\": \"0 0% 98%\",\n      popover: \"0 0% 3.9%\",\n      \"popover-foreground\": \"0 0% 98%\",\n      primary: \"0 0% 98%\",\n      \"primary-foreground\": \"0 0% 9%\",\n      secondary: \"0 0% 14.9%\",\n      \"secondary-foreground\": \"0 0% 98%\",\n      muted: \"0 0% 14.9%\",\n      \"muted-foreground\": \"0 0% 63.9%\",\n      accent: \"0 0% 14.9%\",\n      \"accent-foreground\": \"0 0% 98%\",\n      destructive: \"0 62.8% 30.6%\",\n      \"destructive-foreground\": \"0 0% 98%\",\n      border: \"0 0% 14.9%\",\n      input: \"0 0% 14.9%\",\n      ring: \"0 0% 83.1%\",\n      radius: \"0.75rem\",\n      \"chart-1\": \"220 70% 50%\",\n      \"chart-2\": \"160 60% 45%\",\n      \"chart-3\": \"30 80% 55%\",\n      \"chart-4\": \"280 65% 60%\",\n      \"chart-5\": \"340 75% 55%\",\n    },\n  },\n  red: {\n    light: {\n      background: \"0 0% 100%\",\n      foreground: \"0 0% 3.9%\",\n      card: \"0 0% 100%\",\n      \"card-foreground\": \"0 0% 3.9%\",\n      popover: \"0 0% 100%\",\n      \"popover-foreground\": \"0 0% 3.9%\",\n      primary: \"0 72.2% 50.6%\",\n      \"primary-foreground\": \"0 85.7% 97.3%\",\n      secondary: \"0 0% 96.1%\",\n      \"secondary-foreground\": \"0 0% 9%\",\n      muted: \"0 0% 96.1%\",\n      \"muted-foreground\": \"0 0% 45.1%\",\n      accent: \"0 0% 96.1%\",\n      \"accent-foreground\": \"0 0% 9%\",\n      destructive: \"0 84.2% 60.2%\",\n      \"destructive-foreground\": \"0 0% 98%\",\n      border: \"0 0% 89.8%\",\n      input: \"0 0% 89.8%\",\n      ring: \"0 72.2% 50.6%\",\n      radius: \"0.75rem\",\n      \"chart-1\": \"12 76% 61%\",\n      \"chart-2\": \"173 58% 39%\",\n      \"chart-3\": \"197 37% 24%\",\n      \"chart-4\": \"43 74% 66%\",\n      \"chart-5\": \"27 87% 67%\",\n    },\n    dark: {\n      background: \"0 0% 3.9%\",\n      foreground: \"0 0% 98%\",\n      card: \"0 0% 3.9%\",\n      \"card-foreground\": \"0 0% 98%\",\n      popover: \"0 0% 3.9%\",\n      \"popover-foreground\": \"0 0% 98%\",\n      primary: \"0 72.2% 50.6%\",\n      \"primary-foreground\": \"0 85.7% 97.3%\",\n      secondary: \"0 0% 14.9%\",\n      \"secondary-foreground\": \"0 0% 98%\",\n      muted: \"0 0% 14.9%\",\n      \"muted-foreground\": \"0 0% 63.9%\",\n      accent: \"0 0% 14.9%\",\n      \"accent-foreground\": \"0 0% 98%\",\n      destructive: \"0 62.8% 30.6%\",\n      \"destructive-foreground\": \"0 0% 98%\",\n      border: \"0 0% 14.9%\",\n      input: \"0 0% 14.9%\",\n      ring: \"0 72.2% 50.6%\",\n      radius: \"0.75rem\",\n      \"chart-1\": \"220 70% 50%\",\n      \"chart-2\": \"160 60% 45%\",\n      \"chart-3\": \"30 80% 55%\",\n      \"chart-4\": \"280 65% 60%\",\n      \"chart-5\": \"340 75% 55%\",\n    },\n  },\n  violet: {\n    light: {\n      background: \"0 0% 100%\",\n      foreground: \"224 71.4% 4.1%\",\n      card: \"0 0% 100%\",\n      \"card-foreground\": \"224 71.4% 4.1%\",\n      popover: \"0 0% 100%\",\n      \"popover-foreground\": \"224 71.4% 4.1%\",\n      primary: \"262.1 83.3% 57.8%\",\n      \"primary-foreground\": \"210 20% 98%\",\n      secondary: \"220 14.3% 95.9%\",\n      \"secondary-foreground\": \"220.9 39.3% 11%\",\n      muted: \"220 14.3% 95.9%\",\n      \"muted-foreground\": \"220 8.9% 46.1%\",\n      accent: \"220 14.3% 95.9%\",\n      \"accent-foreground\": \"220.9 39.3% 11%\",\n      destructive: \"0 84.2% 60.2%\",\n      \"destructive-foreground\": \"210 20% 98%\",\n      border: \"220 13% 91%\",\n      input: \"220 13% 91%\",\n      ring: \"262.1 83.3% 57.8%\",\n      radius: \"0.75rem\",\n      \"chart-1\": \"12 76% 61%\",\n      \"chart-2\": \"173 58% 39%\",\n      \"chart-3\": \"197 37% 24%\",\n      \"chart-4\": \"43 74% 66%\",\n      \"chart-5\": \"27 87% 67%\",\n    },\n    dark: {\n      background: \"224 71.4% 4.1%\",\n      foreground: \"210 20% 98%\",\n      card: \"224 71.4% 4.1%\",\n      \"card-foreground\": \"210 20% 98%\",\n      popover: \"224 71.4% 4.1%\",\n      \"popover-foreground\": \"210 20% 98%\",\n      primary: \"263.4 70% 50.4%\",\n      \"primary-foreground\": \"210 20% 98%\",\n      secondary: \"215 27.9% 16.9%\",\n      \"secondary-foreground\": \"210 20% 98%\",\n      muted: \"215 27.9% 16.9%\",\n      \"muted-foreground\": \"217.9 10.6% 64.9%\",\n      accent: \"215 27.9% 16.9%\",\n      \"accent-foreground\": \"210 20% 98%\",\n      destructive: \"0 62.8% 30.6%\",\n      \"destructive-foreground\": \"210 20% 98%\",\n      border: \"215 27.9% 16.9%\",\n      input: \"215 27.9% 16.9%\",\n      ring: \"263.4 70% 50.4%\",\n      radius: \"0.75rem\",\n      \"chart-1\": \"220 70% 50%\",\n      \"chart-2\": \"160 60% 45%\",\n      \"chart-3\": \"30 80% 55%\",\n      \"chart-4\": \"280 65% 60%\",\n      \"chart-5\": \"340 75% 55%\",\n    },\n  },\n  blue: {\n    light: {\n      background: \"0 0% 100%\",\n      foreground: \"222.2 84% 4.9%\",\n      card: \"0 0% 100%\",\n      \"card-foreground\": \"222.2 84% 4.9%\",\n      popover: \"0 0% 100%\",\n      \"popover-foreground\": \"222.2 84% 4.9%\",\n      primary: \"221.2 83.2% 53.3%\",\n      \"primary-foreground\": \"210 40% 98%\",\n      secondary: \"210 40% 96.1%\",\n      \"secondary-foreground\": \"222.2 47.4% 11.2%\",\n      muted: \"210 40% 96.1%\",\n      \"muted-foreground\": \"215.4 16.3% 46.9%\",\n      accent: \"210 40% 96.1%\",\n      \"accent-foreground\": \"222.2 47.4% 11.2%\",\n      destructive: \"0 84.2% 60.2%\",\n      \"destructive-foreground\": \"210 40% 98%\",\n      border: \"214.3 31.8% 91.4%\",\n      input: \"214.3 31.8% 91.4%\",\n      ring: \"221.2 83.2% 53.3%\",\n      radius: \"0.75rem\",\n      \"chart-1\": \"12 76% 61%\",\n      \"chart-2\": \"173 58% 39%\",\n      \"chart-3\": \"197 37% 24%\",\n      \"chart-4\": \"43 74% 66%\",\n      \"chart-5\": \"27 87% 67%\",\n    },\n    dark: {\n      background: \"222.2 84% 4.9%\",\n      foreground: \"210 40% 98%\",\n      card: \"222.2 84% 4.9%\",\n      \"card-foreground\": \"210 40% 98%\",\n      popover: \"222.2 84% 4.9%\",\n      \"popover-foreground\": \"210 40% 98%\",\n      primary: \"217.2 91.2% 59.8%\",\n      \"primary-foreground\": \"222.2 47.4% 11.2%\",\n      secondary: \"217.2 32.6% 17.5%\",\n      \"secondary-foreground\": \"210 40% 98%\",\n      muted: \"217.2 32.6% 17.5%\",\n      \"muted-foreground\": \"215 20.2% 65.1%\",\n      accent: \"217.2 32.6% 17.5%\",\n      \"accent-foreground\": \"210 40% 98%\",\n      destructive: \"0 62.8% 30.6%\",\n      \"destructive-foreground\": \"210 40% 98%\",\n      border: \"217.2 32.6% 17.5%\",\n      input: \"217.2 32.6% 17.5%\",\n      ring: \"224.3 76.3% 48%\",\n      \"chart-1\": \"220 70% 50%\",\n      \"chart-2\": \"160 60% 45%\",\n      \"chart-3\": \"30 80% 55%\",\n      \"chart-4\": \"280 65% 60%\",\n      \"chart-5\": \"340 75% 55%\",\n    },\n  },\n  tangerine: {\n    light: {\n      background: \"0 0% 100%\",\n      foreground: \"20 14.3% 4.1%\",\n      card: \"0 0% 100%\",\n      \"card-foreground\": \"20 14.3% 4.1%\",\n      popover: \"0 0% 100%\",\n      \"popover-foreground\": \"20 14.3% 4.1%\",\n      primary: \"24.6 95% 53.1%\",\n      \"primary-foreground\": \"60 9.1% 97.8%\",\n      secondary: \"60 4.8% 95.9%\",\n      \"secondary-foreground\": \"24 9.8% 10%\",\n      muted: \"60 4.8% 95.9%\",\n      \"muted-foreground\": \"25 5.3% 44.7%\",\n      accent: \"60 4.8% 95.9%\",\n      \"accent-foreground\": \"24 9.8% 10%\",\n      destructive: \"0 84.2% 60.2%\",\n      \"destructive-foreground\": \"60 9.1% 97.8%\",\n      border: \"20 5.9% 90%\",\n      input: \"20 5.9% 90%\",\n      ring: \"24.6 95% 53.1%\",\n      radius: \"0.75rem\",\n      \"chart-1\": \"12 76% 61%\",\n      \"chart-2\": \"173 58% 39%\",\n      \"chart-3\": \"197 37% 24%\",\n      \"chart-4\": \"43 74% 66%\",\n      \"chart-5\": \"27 87% 67%\",\n    },\n    dark: {\n      background: \"20 14.3% 4.1%\",\n      foreground: \"60 9.1% 97.8%\",\n      card: \"20 14.3% 4.1%\",\n      \"card-foreground\": \"60 9.1% 97.8%\",\n      popover: \"20 14.3% 4.1%\",\n      \"popover-foreground\": \"60 9.1% 97.8%\",\n      primary: \"20.5 90.2% 48.2%\",\n      \"primary-foreground\": \"60 9.1% 97.8%\",\n      secondary: \"12 6.5% 15.1%\",\n      \"secondary-foreground\": \"60 9.1% 97.8%\",\n      muted: \"12 6.5% 15.1%\",\n      \"muted-foreground\": \"24 5.4% 63.9%\",\n      accent: \"12 6.5% 15.1%\",\n      \"accent-foreground\": \"60 9.1% 97.8%\",\n      destructive: \"0 72.2% 50.6%\",\n      \"destructive-foreground\": \"60 9.1% 97.8%\",\n      border: \"12 6.5% 15.1%\",\n      input: \"12 6.5% 15.1%\",\n      ring: \"20.5 90.2% 48.2%\",\n      \"chart-1\": \"220 70% 50%\",\n      \"chart-2\": \"160 60% 45%\",\n      \"chart-3\": \"30 80% 55%\",\n      \"chart-4\": \"280 65% 60%\",\n      \"chart-5\": \"340 75% 55%\",\n    },\n  },\n  emerald: {\n    light: {\n      background: \"0 0% 100%\",\n      foreground: \"240 10% 3.9%\",\n      card: \"0 0% 100%\",\n      \"card-foreground\": \"240 10% 3.9%\",\n      popover: \"0 0% 100%\",\n      \"popover-foreground\": \"240 10% 3.9%\",\n      primary: \"142.1 76.2% 36.3%\",\n      \"primary-foreground\": \"355.7 100% 97.3%\",\n      secondary: \"240 4.8% 95.9%\",\n      \"secondary-foreground\": \"240 5.9% 10%\",\n      muted: \"240 4.8% 95.9%\",\n      \"muted-foreground\": \"240 3.8% 46.1%\",\n      accent: \"240 4.8% 95.9%\",\n      \"accent-foreground\": \"240 5.9% 10%\",\n      destructive: \"0 84.2% 60.2%\",\n      \"destructive-foreground\": \"0 0% 98%\",\n      border: \"240 5.9% 90%\",\n      input: \"240 5.9% 90%\",\n      ring: \"142.1 76.2% 36.3%\",\n      radius: \"0.75rem\",\n      \"chart-1\": \"12 76% 61%\",\n      \"chart-2\": \"173 58% 39%\",\n      \"chart-3\": \"197 37% 24%\",\n      \"chart-4\": \"43 74% 66%\",\n      \"chart-5\": \"27 87% 67%\",\n    },\n    dark: {\n      background: \"20 14.3% 4.1%\",\n      foreground: \"0 0% 95%\",\n      card: \"24 9.8% 10%\",\n      \"card-foreground\": \"0 0% 95%\",\n      popover: \"0 0% 9%\",\n      \"popover-foreground\": \"0 0% 95%\",\n      primary: \"142.1 70.6% 45.3%\",\n      \"primary-foreground\": \"144.9 80.4% 10%\",\n      secondary: \"240 3.7% 15.9%\",\n      \"secondary-foreground\": \"0 0% 98%\",\n      muted: \"0 0% 15%\",\n      \"muted-foreground\": \"240 5% 64.9%\",\n      accent: \"12 6.5% 15.1%\",\n      \"accent-foreground\": \"0 0% 98%\",\n      destructive: \"0 62.8% 30.6%\",\n      \"destructive-foreground\": \"0 85.7% 97.3%\",\n      border: \"240 3.7% 15.9%\",\n      input: \"240 3.7% 15.9%\",\n      ring: \"142.4 71.8% 29.2%\",\n      \"chart-1\": \"220 70% 50%\",\n      \"chart-2\": \"160 60% 45%\",\n      \"chart-3\": \"30 80% 55%\",\n      \"chart-4\": \"280 65% 60%\",\n      \"chart-5\": \"340 75% 55%\",\n    },\n  },\n  amber: {\n    light: {\n      background: \"0 0% 100%\",\n      foreground: \"20 14.3% 4.1%\",\n      card: \"0 0% 100%\",\n      \"card-foreground\": \"20 14.3% 4.1%\",\n      popover: \"0 0% 100%\",\n      \"popover-foreground\": \"20 14.3% 4.1%\",\n      primary: \"47.9 95.8% 53.1%\",\n      \"primary-foreground\": \"26 83.3% 14.1%\",\n      secondary: \"60 4.8% 95.9%\",\n      \"secondary-foreground\": \"24 9.8% 10%\",\n      muted: \"60 4.8% 95.9%\",\n      \"muted-foreground\": \"25 5.3% 44.7%\",\n      accent: \"60 4.8% 95.9%\",\n      \"accent-foreground\": \"24 9.8% 10%\",\n      destructive: \"0 84.2% 60.2%\",\n      \"destructive-foreground\": \"60 9.1% 97.8%\",\n      border: \"20 5.9% 90%\",\n      input: \"20 5.9% 90%\",\n      ring: \"20 14.3% 4.1%\",\n      radius: \"0.75rem\",\n      \"chart-1\": \"12 76% 61%\",\n      \"chart-2\": \"173 58% 39%\",\n      \"chart-3\": \"197 37% 24%\",\n      \"chart-4\": \"43 74% 66%\",\n      \"chart-5\": \"27 87% 67%\",\n    },\n    dark: {\n      background: \"20 14.3% 4.1%\",\n      foreground: \"60 9.1% 97.8%\",\n      card: \"20 14.3% 4.1%\",\n      \"card-foreground\": \"60 9.1% 97.8%\",\n      popover: \"20 14.3% 4.1%\",\n      \"popover-foreground\": \"60 9.1% 97.8%\",\n      primary: \"47.9 95.8% 53.1%\",\n      \"primary-foreground\": \"26 83.3% 14.1%\",\n      secondary: \"12 6.5% 15.1%\",\n      \"secondary-foreground\": \"60 9.1% 97.8%\",\n      muted: \"12 6.5% 15.1%\",\n      \"muted-foreground\": \"24 5.4% 63.9%\",\n      accent: \"12 6.5% 15.1%\",\n      \"accent-foreground\": \"60 9.1% 97.8%\",\n      destructive: \"0 62.8% 30.6%\",\n      \"destructive-foreground\": \"60 9.1% 97.8%\",\n      border: \"12 6.5% 15.1%\",\n      input: \"12 6.5% 15.1%\",\n      ring: \"35.5 91.7% 32.9%\",\n      \"chart-1\": \"220 70% 50%\",\n      \"chart-2\": \"160 60% 45%\",\n      \"chart-3\": \"30 80% 55%\",\n      \"chart-4\": \"280 65% 60%\",\n      \"chart-5\": \"340 75% 55%\",\n    },\n  },\n};\n"
  },
  {
    "path": "customer-support-agent/tailwind.config.ts",
    "content": "import type { Config } from \"tailwindcss\";\n\nconst config = {\n  darkMode: [\"class\"],\n  content: [\n    \"./pages/**/*.{ts,tsx}\",\n    \"./components/**/*.{ts,tsx}\",\n    \"./app/**/*.{ts,tsx}\",\n    \"./src/**/*.{ts,tsx}\",\n  ],\n  prefix: \"\",\n  theme: {\n    container: {\n      center: true,\n      padding: \"2rem\",\n      screens: {\n        \"2xl\": \"1400px\",\n      },\n    },\n    extend: {\n      colors: {\n        border: \"hsl(var(--border))\",\n        input: \"hsl(var(--input))\",\n        ring: \"hsl(var(--ring))\",\n        background: \"hsl(var(--background))\",\n        foreground: \"hsl(var(--foreground))\",\n        primary: {\n          DEFAULT: \"hsl(var(--primary))\",\n          foreground: \"hsl(var(--primary-foreground))\",\n        },\n        secondary: {\n          DEFAULT: \"hsl(var(--secondary))\",\n          foreground: \"hsl(var(--secondary-foreground))\",\n        },\n        destructive: {\n          DEFAULT: \"hsl(var(--destructive))\",\n          foreground: \"hsl(var(--destructive-foreground))\",\n        },\n        muted: {\n          DEFAULT: \"hsl(var(--muted))\",\n          foreground: \"hsl(var(--muted-foreground))\",\n        },\n        accent: {\n          DEFAULT: \"hsl(var(--accent))\",\n          foreground: \"hsl(var(--accent-foreground))\",\n        },\n        popover: {\n          DEFAULT: \"hsl(var(--popover))\",\n          foreground: \"hsl(var(--popover-foreground))\",\n        },\n        card: {\n          DEFAULT: \"hsl(var(--card))\",\n          foreground: \"hsl(var(--card-foreground))\",\n        },\n      },\n      borderRadius: {\n        lg: \"var(--radius)\",\n        md: \"calc(var(--radius) - 2px)\",\n        sm: \"calc(var(--radius) - 4px)\",\n      },\n      keyframes: {\n        \"accordion-down\": {\n          from: { height: \"0\" },\n          to: { height: \"var(--radix-accordion-content-height)\" },\n        },\n        \"accordion-up\": {\n          from: { height: \"var(--radix-accordion-content-height)\" },\n          to: { height: \"0\" },\n        },\n        fadeIn: {\n          \"0%\": { opacity: \"0\" },\n          \"100%\": { opacity: \"1\" },\n        },\n        fadeInUp: {\n          \"0%\": { opacity: \"0\", transform: \"translateY(8px)\" },\n          \"100%\": { opacity: \"1\", transform: \"translateY(0)\" },\n        },\n        shimmer: {\n          \"100%\": { transform: \"translateX(100%)\" },\n        },\n      },\n      animation: {\n        \"accordion-down\": \"accordion-down 0.2s ease-out\",\n        \"accordion-up\": \"accordion-up 0.2s ease-out\",\n        \"fade-in\": \"fadeIn 0.5s ease-out forwards\",\n        \"fade-in-up\": \"fadeInUp 0.5s ease-out forwards\",\n        \"fade-in-up-fast\": \"fadeInUp 0.3s ease-out forwards\",\n        \"fade-in-up-slow\":\n          \"fadeInUp 0.8s cubic-bezier(0.4, 0, 0.2, 1) forwards\",\n        shimmer: \"shimmer 2s linear infinite\",\n      },\n    },\n  },\n  plugins: [require(\"tailwindcss-animate\")],\n} satisfies Config;\n\nexport default config;\n"
  },
  {
    "path": "customer-support-agent/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "financial-data-analyst/.eslintrc.json",
    "content": "{\n  \"extends\": \"next\",\n  \"rules\": {\n    \"react/no-unescaped-entities\": \"off\",\n    \"@next/next/no-page-custom-font\": \"off\"\n  }\n}\n"
  },
  {
    "path": "financial-data-analyst/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n.yarn/install-state.gz\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "financial-data-analyst/README.md",
    "content": "# Claude Financial Data Analyst\n\n![hero](public/hero.png)\n\nA sophisticated Next.js application that combines Claude's capabilities with interactive data visualization to analyze financial data via chat.\n\n## Features\n\n- **Intelligent Data Analysis**: Powered by Claude (Using Claude 3 Haiku & Claude 3.5 Sonnet)\n- **Multi-Format File Upload Support**:\n  - Text/Code files (.txt, .md, .html, .py, .csv, etc)\n  - PDF documents (Regular PDF with text, scanned documents not supported)\n  - Images\n- **Interactive Data Visualization**: Based on the context and data analyzed, Claude can generate the following charts:\n  - Line Charts (Time series data & trends)\n  - Bar Charts (Single metric comparisons)\n  - Multi-Bar Charts (Multiple metrics comparison)\n  - Area Charts (Volume/quantity over time)\n  - Stacked Area Charts (Component breakdowns)\n  - Pie Charts (Distribution analysis)\n\n## Getting Started\n\n### Prerequisites\n\n- Node.js 18+ installed\n- Claude API key (For Claude integration)\n\n### Installation\n\n1. Clone the repository:\n```bash\ngit clone https://github.com/anthropics/anthropic-quickstarts.git\ncd anthropic-quickstarts/financial-data-analyst\n```\n\n2. Install dependencies:\n```bash\nnpm install\n```\n\n3. Create a `.env.local` file in the root directory:\n```env\nANTHROPIC_API_KEY=your_api_key_here\n```\n\n4. Run the development server:\n```bash\nnpm run dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000) with your browser to see the result.\n\n## Technology Stack\n\n- **Frontend**:\n  - Next.js 14\n  - React\n  - TailwindCSS\n  - Shadcn/ui Components\n  - Recharts (For data visualization)\n  - PDF.js (For PDF processing)\n\n- **Backend**:\n  - Next.js API Routes\n  - Edge Runtime\n  - Anthropic SDK\n\n## Usage Examples\n\nThe assistant can help with various financial analysis tasks:\n\n1. **Data Extraction & Analysis**:\n   - Upload financial documents\n   - Extract key metrics\n   - Analyze trends and patterns\n\n2. **Visualization Creation**:\n   - Generate charts based on data\n   - Customize visualizations\n   - Compare multiple metrics\n\n3. **Interactive Analysis**:\n   - Ask questions about the data\n   - Request specific visualizations\n   - Get detailed explanations\n\n## Interesting Use Cases\n\nWhile primarily designed for financial analysis, the AI assistant can be adapted for various intriguing applications:\n\n1. **Environmental Data Analysis**:\n   - Analyze climate change trends\n   - Visualize pollution levels over time\n   - Compare renewable energy adoption across regions\n\n2. **Sports Performance Tracking**:\n   - Upload athlete performance data\n   - Generate visualizations of key metrics\n   - Analyze trends and patterns in team statistics\n\n3. **Social Media Analytics**:\n   - Process engagement data from various platforms\n   - Create charts showing follower growth and interaction rates\n   - Analyze sentiment trends in user comments\n\n4. **Educational Progress Tracking**:\n   - Upload student performance data\n   - Visualize learning progress over time\n   - Compare different teaching methods or curriculums\n\n5. **Health and Fitness Monitoring**:\n   - Process personal health data from wearables\n   - Create charts for metrics like steps, heart rate, and sleep patterns\n   - Analyze long-term health trends and provide insights\n\nYou can even use charts and images to create interesting results, like the ability to see what's most common inside a picture using a pie chart.\n\n![Image Analysis](public/image-analysis.png)\n\n"
  },
  {
    "path": "financial-data-analyst/app/api/finance/route.ts",
    "content": "// app/api/finance/route.ts\nimport { NextRequest } from \"next/server\";\nimport Anthropic from \"@anthropic-ai/sdk\";\nimport type { ChartData } from \"@/types/chart\";\n\n// Initialize Anthropic client with correct headers\nconst anthropic = new Anthropic({\n  apiKey: process.env.ANTHROPIC_API_KEY!,\n});\n\nexport const runtime = \"edge\";\n\n// Helper to validate base64\nconst isValidBase64 = (str: string) => {\n  try {\n    return btoa(atob(str)) === str;\n  } catch (err) {\n    return false;\n  }\n};\n\n// Add Type Definitions\ninterface ChartToolResponse extends ChartData {\n  // Any additional properties specific to the tool response\n}\n\ninterface ToolSchema {\n  name: string;\n  description: string;\n  input_schema: {\n    type: \"object\";\n    properties: Record<string, unknown>;\n    required: string[];\n  };\n}\n\nconst tools: ToolSchema[] = [\n  {\n    name: \"generate_graph_data\",\n    description:\n      \"Generate structured JSON data for creating financial charts and graphs.\",\n    input_schema: {\n      type: \"object\" as const,\n      properties: {\n        chartType: {\n          type: \"string\" as const,\n          enum: [\n            \"bar\",\n            \"multiBar\",\n            \"line\",\n            \"pie\",\n            \"area\",\n            \"stackedArea\",\n          ] as const,\n          description: \"The type of chart to generate\",\n        },\n        config: {\n          type: \"object\" as const,\n          properties: {\n            title: { type: \"string\" as const },\n            description: { type: \"string\" as const },\n            trend: {\n              type: \"object\" as const,\n              properties: {\n                percentage: { type: \"number\" as const },\n                direction: {\n                  type: \"string\" as const,\n                  enum: [\"up\", \"down\"] as const,\n                },\n              },\n              required: [\"percentage\", \"direction\"],\n            },\n            footer: { type: \"string\" as const },\n            totalLabel: { type: \"string\" as const },\n            xAxisKey: { type: \"string\" as const },\n          },\n          required: [\"title\", \"description\"],\n        },\n        data: {\n          type: \"array\" as const,\n          items: {\n            type: \"object\" as const,\n            additionalProperties: true, // Allow any structure\n          },\n        },\n        chartConfig: {\n          type: \"object\" as const,\n          additionalProperties: {\n            type: \"object\" as const,\n            properties: {\n              label: { type: \"string\" as const },\n              stacked: { type: \"boolean\" as const },\n            },\n            required: [\"label\"],\n          },\n        },\n      },\n      required: [\"chartType\", \"config\", \"data\", \"chartConfig\"],\n    },\n  },\n];\n\nexport async function POST(req: NextRequest) {\n  try {\n    const { messages, fileData, model } = await req.json();\n\n    console.log(\"🔍 Initial Request Data:\", {\n      hasMessages: !!messages,\n      messageCount: messages?.length,\n      hasFileData: !!fileData,\n      fileType: fileData?.mediaType,\n      model,\n    });\n\n    // Input validation\n    if (!messages || !Array.isArray(messages)) {\n      return new Response(\n        JSON.stringify({ error: \"Messages array is required\" }),\n        { status: 400 },\n      );\n    }\n\n    if (!model) {\n      return new Response(\n        JSON.stringify({ error: \"Model selection is required\" }),\n        { status: 400 },\n      );\n    }\n\n    // Convert all previous messages\n    let anthropicMessages = messages.map((msg: any) => ({\n      role: msg.role,\n      content: msg.content,\n    }));\n\n    // Handle file in the latest message\n    if (fileData) {\n      const { base64, mediaType, isText } = fileData;\n\n      if (!base64) {\n        console.error(\"❌ No base64 data received\");\n        return new Response(JSON.stringify({ error: \"No file data\" }), {\n          status: 400,\n        });\n      }\n\n      try {\n        if (isText) {\n          // Decode base64 text content\n          const textContent = decodeURIComponent(escape(atob(base64)));\n\n          // Replace only the last message with the file content\n          anthropicMessages[anthropicMessages.length - 1] = {\n            role: \"user\",\n            content: [\n              {\n                type: \"text\",\n                text: `File contents of ${fileData.fileName}:\\n\\n${textContent}`,\n              },\n              {\n                type: \"text\",\n                text: messages[messages.length - 1].content,\n              },\n            ],\n          };\n        } else if (mediaType.startsWith(\"image/\")) {\n          // Handle image files\n          anthropicMessages[anthropicMessages.length - 1] = {\n            role: \"user\",\n            content: [\n              {\n                type: \"image\",\n                source: {\n                  type: \"base64\",\n                  media_type: mediaType,\n                  data: base64,\n                },\n              },\n              {\n                type: \"text\",\n                text: messages[messages.length - 1].content,\n              },\n            ],\n          };\n        }\n      } catch (error) {\n        console.error(\"Error processing file content:\", error);\n        return new Response(\n          JSON.stringify({ error: \"Failed to process file content\" }),\n          { status: 400 },\n        );\n      }\n    }\n\n    console.log(\"🚀 Final Claude API Request:\", {\n      endpoint: \"messages.create\",\n      model,\n      max_tokens: 4096,\n      temperature: 0.7,\n      messageCount: anthropicMessages.length,\n      tools: tools.map((t) => t.name),\n      messageStructure: JSON.stringify(\n        anthropicMessages.map((msg) => ({\n          role: msg.role,\n          content:\n            typeof msg.content === \"string\"\n              ? msg.content.slice(0, 50) + \"...\"\n              : \"[Complex Content]\",\n        })),\n        null,\n        2,\n      ),\n    });\n\n    const response = await anthropic.messages.create({\n      model,\n      max_tokens: 4096,\n      temperature: 0.7,\n      tools: tools,\n      tool_choice: { type: \"auto\" },\n      messages: anthropicMessages,\n      system: `You are a financial data visualization expert. Your role is to analyze financial data and create clear, meaningful visualizations using generate_graph_data tool:\n\nHere are the chart types available and their ideal use cases:\n\n1. LINE CHARTS (\"line\")\n   - Time series data showing trends\n   - Financial metrics over time\n   - Market performance tracking\n\n2. BAR CHARTS (\"bar\")\n   - Single metric comparisons\n   - Period-over-period analysis\n   - Category performance\n\n3. MULTI-BAR CHARTS (\"multiBar\")\n   - Multiple metrics comparison\n   - Side-by-side performance analysis\n   - Cross-category insights\n\n4. AREA CHARTS (\"area\")\n   - Volume or quantity over time\n   - Cumulative trends\n   - Market size evolution\n\n5. STACKED AREA CHARTS (\"stackedArea\")\n   - Component breakdowns over time\n   - Portfolio composition changes\n   - Market share evolution\n\n6. PIE CHARTS (\"pie\")\n   - Distribution analysis\n   - Market share breakdown\n   - Portfolio allocation\n\nWhen generating visualizations:\n1. Structure data correctly based on the chart type\n2. Use descriptive titles and clear descriptions\n3. Include trend information when relevant (percentage and direction)\n4. Add contextual footer notes\n5. Use proper data keys that reflect the actual metrics\n\nData Structure Examples:\n\nFor Time-Series (Line/Bar/Area):\n{\n  data: [\n    { period: \"Q1 2024\", revenue: 1250000 },\n    { period: \"Q2 2024\", revenue: 1450000 }\n  ],\n  config: {\n    xAxisKey: \"period\",\n    title: \"Quarterly Revenue\",\n    description: \"Revenue growth over time\"\n  },\n  chartConfig: {\n    revenue: { label: \"Revenue ($)\" }\n  }\n}\n\nFor Comparisons (MultiBar):\n{\n  data: [\n    { category: \"Product A\", sales: 450000, costs: 280000 },\n    { category: \"Product B\", sales: 650000, costs: 420000 }\n  ],\n  config: {\n    xAxisKey: \"category\",\n    title: \"Product Performance\",\n    description: \"Sales vs Costs by Product\"\n  },\n  chartConfig: {\n    sales: { label: \"Sales ($)\" },\n    costs: { label: \"Costs ($)\" }\n  }\n}\n\nFor Distributions (Pie):\n{\n  data: [\n    { segment: \"Equities\", value: 5500000 },\n    { segment: \"Bonds\", value: 3200000 }\n  ],\n  config: {\n    xAxisKey: \"segment\",\n    title: \"Portfolio Allocation\",\n    description: \"Current investment distribution\",\n    totalLabel: \"Total Assets\"\n  },\n  chartConfig: {\n    equities: { label: \"Equities\" },\n    bonds: { label: \"Bonds\" }\n  }\n}\n\nAlways:\n- Generate real, contextually appropriate data\n- Use proper financial formatting\n- Include relevant trends and insights\n- Structure data exactly as needed for the chosen chart type\n- Choose the most appropriate visualization for the data\n\nNever:\n- Use placeholder or static data\n- Announce the tool usage\n- Include technical implementation details in responses\n- NEVER SAY you are using the generate_graph_data tool, just execute it when needed.\n\nFocus on clear financial insights and let the visualization enhance understanding.`,\n    });\n\n    console.log(\"✅ Claude API Response received:\", {\n      status: \"success\",\n      stopReason: response.stop_reason,\n      hasToolUse: response.content.some((c) => c.type === \"tool_use\"),\n      contentTypes: response.content.map((c) => c.type),\n      contentLength:\n        response.content[0].type === \"text\"\n          ? response.content[0].text.length\n          : 0,\n      toolOutput: response.content.find((c) => c.type === \"tool_use\")\n        ? JSON.stringify(\n            response.content.find((c) => c.type === \"tool_use\"),\n            null,\n            2,\n          )\n        : \"No tool used\",\n    });\n\n    const toolUseContent = response.content.find((c) => c.type === \"tool_use\");\n    const textContent = response.content.find((c) => c.type === \"text\");\n\n    const processToolResponse = (toolUseContent: any) => {\n      if (!toolUseContent) return null;\n\n      const chartData = toolUseContent.input as ChartToolResponse;\n\n      if (\n        !chartData.chartType ||\n        !chartData.data ||\n        !Array.isArray(chartData.data)\n      ) {\n        throw new Error(\"Invalid chart data structure\");\n      }\n\n      // Transform data for pie charts to match expected structure\n      if (chartData.chartType === \"pie\") {\n        // Ensure data items have 'segment' and 'value' keys\n        chartData.data = chartData.data.map((item) => {\n          // Find the first key in chartConfig (e.g., 'sales')\n          const valueKey = Object.keys(chartData.chartConfig)[0];\n          const segmentKey = chartData.config.xAxisKey || \"segment\";\n\n          return {\n            segment:\n              item[segmentKey] || item.segment || item.category || item.name,\n            value: item[valueKey] || item.value,\n          };\n        });\n\n        // Ensure xAxisKey is set to 'segment' for consistency\n        chartData.config.xAxisKey = \"segment\";\n      }\n\n      // Create new chartConfig with system color variables\n      const processedChartConfig = Object.entries(chartData.chartConfig).reduce(\n        (acc, [key, config], index) => ({\n          ...acc,\n          [key]: {\n            ...config,\n            // Assign color variables sequentially\n            color: `hsl(var(--chart-${index + 1}))`,\n          },\n        }),\n        {},\n      );\n\n      return {\n        ...chartData,\n        chartConfig: processedChartConfig,\n      };\n    };\n\n    const processedChartData = toolUseContent\n      ? processToolResponse(toolUseContent)\n      : null;\n\n    return new Response(\n      JSON.stringify({\n        content: textContent?.text || \"\",\n        hasToolUse: response.content.some((c) => c.type === \"tool_use\"),\n        toolUse: toolUseContent,\n        chartData: processedChartData,\n      }),\n      {\n        headers: {\n          \"Content-Type\": \"application/json\",\n          \"Cache-Control\": \"no-cache\",\n        },\n      },\n    );\n  } catch (error) {\n    console.error(\"❌ Finance API Error: \", error);\n    console.error(\"Full error details:\", {\n      name: error instanceof Error ? error.name : \"Unknown\",\n      message: error instanceof Error ? error.message : \"Unknown error\",\n      stack: error instanceof Error ? error.stack : undefined,\n      headers: error instanceof Error ? (error as any).headers : undefined,\n      response: error instanceof Error ? (error as any).response : undefined,\n    });\n\n    // Add specific error handling for different scenarios\n    if (error instanceof Anthropic.APIError) {\n      return new Response(\n        JSON.stringify({\n          error: \"API Error\",\n          details: error.message,\n          code: error.status,\n        }),\n        { status: error.status },\n      );\n    }\n\n    if (error instanceof Anthropic.AuthenticationError) {\n      return new Response(\n        JSON.stringify({\n          error: \"Authentication Error\",\n          details: \"Invalid API key or authentication failed\",\n        }),\n        { status: 401 },\n      );\n    }\n\n    return new Response(\n      JSON.stringify({\n        error:\n          error instanceof Error ? error.message : \"An unknown error occurred\",\n      }),\n      {\n        status: 500,\n        headers: { \"Content-Type\": \"application/json\" },\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "financial-data-analyst/app/finance/page.tsx",
    "content": "// app/finance/page.tsx\n\"use client\";\n\nimport React, { useState, useRef, useEffect, useCallback } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Card,\n  CardContent,\n  CardHeader,\n  CardTitle,\n  CardDescription,\n  CardFooter,\n} from \"@/components/ui/card\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport {\n  Send,\n  ChevronDown,\n  Paperclip,\n  ChartLine,\n  ChartArea,\n  FileInput,\n  MessageCircleQuestion,\n  ChartColumnBig,\n} from \"lucide-react\";\nimport FilePreview from \"@/components/FilePreview\";\nimport { ChartRenderer } from \"@/components/ChartRenderer\";\nimport { toast } from \"@/hooks/use-toast\";\nimport { Badge } from \"@/components/ui/badge\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport type { ChartData } from \"@/types/chart\";\nimport TopNavBar from \"@/components/TopNavBar\";\nimport {\n  readFileAsText,\n  readFileAsBase64,\n  readFileAsPDFText,\n} from \"@/utils/fileHandling\";\n\n// Types\ninterface Message {\n  id: string;\n  role: string;\n  content: string;\n  hasToolUse?: boolean;\n  file?: {\n    base64: string;\n    fileName: string;\n    mediaType: string;\n    isText?: boolean;\n  };\n  chartData?: ChartData;\n}\n\ntype Model = {\n  id: string;\n  name: string;\n};\n\ninterface FileUpload {\n  base64: string;\n  fileName: string;\n  mediaType: string;\n  isText?: boolean;\n  fileSize?: number;\n}\n\nconst models: Model[] = [\n  { id: \"claude-3-haiku-20240307\", name: \"Claude 3 Haiku\" },\n  { id: \"claude-haiku-4-5-20251001\", name: \"Claude 4.5 Haiku\" },\n  { id: \"claude-3-5-sonnet-20240620\", name: \"Claude 3.5 Sonnet\" },\n];\n\n// Updated APIResponse interface\ninterface APIResponse {\n  content: string;\n  hasToolUse: boolean;\n  toolUse?: {\n    type: \"tool_use\";\n    id: string;\n    name: string;\n    input: ChartData;\n  };\n  chartData?: ChartData;\n}\n\ninterface MessageComponentProps {\n  message: Message;\n}\n\nconst SafeChartRenderer: React.FC<{ data: ChartData }> = ({ data }) => {\n  try {\n    return (\n      <div className=\"w-full h-full p-6 flex flex-col\">\n        <div className=\"w-[90%] flex-1 mx-auto\">\n          <ChartRenderer data={data} />\n        </div>\n      </div>\n    );\n  } catch (error) {\n    console.error(\"Chart rendering error:\", error);\n    const errorMessage =\n      error instanceof Error ? error.message : \"An unknown error occurred\";\n    return (\n      <div className=\"text-red-500\">Error rendering chart: {errorMessage}</div>\n    );\n  }\n};\n\nconst MessageComponent: React.FC<MessageComponentProps> = ({ message }) => {\n  console.log(\"Message with chart data:\", message); // Add this line for debugging\n  return (\n    <div className=\"flex items-start gap-2\">\n      {message.role === \"assistant\" && (\n        <Avatar className=\"w-8 h-8 border\">\n          <AvatarImage src=\"/ant-logo.svg\" alt=\"AI Assistant Avatar\" />\n          <AvatarFallback>AI</AvatarFallback>\n        </Avatar>\n      )}\n      <div\n        className={`flex flex-col max-w-[75%] ${\n          message.role === \"user\" ? \"ml-auto\" : \"\"\n        }`}\n      >\n        <div\n          className={`p-3 rounded-md text-base ${\n            message.role === \"user\"\n              ? \"bg-primary text-primary-foreground\"\n              : \"bg-muted border\"\n          }`}\n        >\n          {message.content === \"thinking\" ? (\n            <div className=\"flex items-center\">\n              <div className=\"animate-spin rounded-full h-4 w-4 border-b-2 border-gray-900 mr-2\" />\n              {message.hasToolUse ? (\n                <div className=\"flex flex-col gap-2\">\n                  <Badge variant=\"secondary\" className=\"inline-flex\">\n                    <ChartLine className=\"w-4 h-4 mr-1\" /> Generated Chart\n                  </Badge>\n                  <span>Thinking...</span>\n                </div>\n              ) : (\n                <span>Thinking...</span>\n              )}\n            </div>\n          ) : message.role === \"assistant\" ? (\n            <div className=\"flex flex-col gap-2\">\n              {message.hasToolUse && (\n                <Badge variant=\"secondary\" className=\"inline-flex px-0\">\n                  <ChartLine className=\"w-4 h-4 mr-1\" /> Generated Chart\n                </Badge>\n              )}\n              <span>{message.content}</span>\n            </div>\n          ) : (\n            <span>{message.content}</span>\n          )}\n        </div>\n        {message.file && (\n          <div className=\"mt-1.5\">\n            <FilePreview file={message.file} size=\"small\" />\n          </div>\n        )}\n      </div>\n    </div>\n  );\n};\n\nconst ChartPagination = ({\n  total,\n  current,\n  onDotClick,\n}: {\n  total: number;\n  current: number;\n  onDotClick: (index: number) => void;\n}) => (\n  <div className=\"fixed right-12 top-1/2 -translate-y-1/2 flex flex-col gap-2\">\n    {Array.from({ length: total }).map((_, i) => (\n      <button\n        key={i}\n        onClick={() => onDotClick(i)}\n        className={`w-2 h-2 rounded-full transition-all ${\n          i === current\n            ? \"bg-primary scale-125\"\n            : \"bg-muted hover:bg-primary/50\"\n        }`}\n      />\n    ))}\n  </div>\n);\n\nexport default function AIChat() {\n  const [messages, setMessages] = useState<Message[]>([]);\n  const [input, setInput] = useState(\"\");\n  const [isLoading, setIsLoading] = useState(false);\n  const [selectedModel, setSelectedModel] = useState(\n    \"claude-3-5-sonnet-20240620\",\n  );\n  const messagesEndRef = useRef<HTMLDivElement>(null);\n  const chartEndRef = useRef<HTMLDivElement>(null);\n  const fileInputRef = useRef<HTMLInputElement>(null);\n  const [currentUpload, setCurrentUpload] = useState<FileUpload | null>(null);\n  const [isUploading, setIsUploading] = useState(false);\n  const [currentChartIndex, setCurrentChartIndex] = useState(0);\n  const contentRef = useRef<HTMLDivElement>(null);\n  const [isScrollLocked, setIsScrollLocked] = useState(false);\n\n  useEffect(() => {\n    const scrollToBottom = () => {\n      if (!messagesEndRef.current) return;\n\n      // Use requestAnimationFrame to ensure DOM has updated\n      requestAnimationFrame(() => {\n        messagesEndRef.current?.scrollIntoView({\n          behavior: \"smooth\",\n          block: \"end\",\n        });\n      });\n    };\n\n    // Scroll when messages change or when loading state changes\n    const timeoutId = setTimeout(scrollToBottom, 100);\n\n    return () => clearTimeout(timeoutId);\n  }, [messages, isLoading]); // Add isLoading to dependencies\n\n  useEffect(() => {\n    if (!messagesEndRef.current) return;\n\n    const observer = new ResizeObserver(() => {\n      if (!isScrollLocked) {\n        messagesEndRef.current?.scrollIntoView({\n          behavior: \"smooth\",\n          block: \"end\",\n        });\n      }\n    });\n\n    observer.observe(messagesEndRef.current);\n\n    return () => observer.disconnect();\n  }, [isScrollLocked]);\n\n  const handleChartScroll = useCallback(() => {\n    if (!contentRef.current) return;\n\n    const { scrollTop, clientHeight } = contentRef.current;\n    const newIndex = Math.round(scrollTop / clientHeight);\n    setCurrentChartIndex(newIndex);\n  }, []);\n\n  const scrollToChart = (index: number) => {\n    if (!contentRef.current) return;\n\n    const targetScroll = index * contentRef.current.clientHeight;\n    contentRef.current.scrollTo({\n      top: targetScroll,\n      behavior: \"smooth\",\n    });\n  };\n\n  useEffect(() => {\n    const scrollToNewestChart = () => {\n      const chartsCount = messages.filter((m) => m.chartData).length;\n      if (chartsCount > 0) {\n        setCurrentChartIndex(chartsCount - 1);\n        scrollToChart(chartsCount - 1);\n      }\n    };\n\n    const lastChartIndex = messages.findLastIndex((m) => m.chartData);\n    if (lastChartIndex !== -1) {\n      setTimeout(scrollToNewestChart, 100);\n    }\n  }, [messages]);\n\n  const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {\n    const file = e.target.files?.[0];\n    if (!file) return;\n\n    setIsUploading(true);\n\n    // Create a ref to store the toast handlers\n    let loadingToastRef: { dismiss: () => void } | undefined;\n\n    if (file.type === \"application/pdf\") {\n      loadingToastRef = toast({\n        title: \"Processing PDF\",\n        description: \"Extracting text content...\",\n        duration: Infinity, // This will keep the toast until we dismiss it\n      });\n    }\n\n    try {\n      const isImage = file.type.startsWith(\"image/\");\n      const isPDF = file.type === \"application/pdf\";\n      let base64Data = \"\";\n      let isText = false;\n\n      if (isImage) {\n        base64Data = await readFileAsBase64(file);\n        isText = false;\n      } else if (isPDF) {\n        try {\n          const pdfText = await readFileAsPDFText(file);\n          base64Data = btoa(encodeURIComponent(pdfText));\n          isText = true;\n        } catch (error) {\n          console.error(\"Failed to parse PDF:\", error);\n          toast({\n            title: \"PDF parsing failed\",\n            description: \"Unable to extract text from the PDF\",\n            variant: \"destructive\",\n          });\n          return;\n        }\n      } else {\n        try {\n          const textContent = await readFileAsText(file);\n          base64Data = btoa(encodeURIComponent(textContent));\n          isText = true;\n        } catch (error) {\n          console.error(\"Failed to read as text:\", error);\n          toast({\n            title: \"Invalid file type\",\n            description: \"File must be readable as text, PDF, or be an image\",\n            variant: \"destructive\",\n          });\n          return;\n        }\n      }\n\n      setCurrentUpload({\n        base64: base64Data,\n        fileName: file.name,\n        mediaType: isText ? \"text/plain\" : file.type,\n        isText,\n      });\n\n      toast({\n        title: \"File uploaded\",\n        description: `${file.name} ready to analyze`,\n      });\n    } catch (error) {\n      console.error(\"Error processing file:\", error);\n      toast({\n        title: \"Upload failed\",\n        description: \"Failed to process the file\",\n        variant: \"destructive\",\n      });\n    } finally {\n      setIsUploading(false);\n      if (loadingToastRef) {\n        loadingToastRef.dismiss(); // Use the dismiss method from the toast ref\n        // Show success toast for PDF\n        if (file.type === \"application/pdf\") {\n          toast({\n            title: \"PDF Processed\",\n            description: \"Text extracted successfully\",\n          });\n        }\n      }\n    }\n  };\n\n  const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n    event.preventDefault();\n    if (!input.trim() && !currentUpload) return;\n    if (isLoading) return;\n\n    setIsScrollLocked(true);\n\n    const userMessage: Message = {\n      id: crypto.randomUUID(),\n      role: \"user\",\n      content: input,\n      file: currentUpload || undefined,\n    };\n\n    const thinkingMessage: Message = {\n      id: crypto.randomUUID(),\n      role: \"assistant\",\n      content: \"thinking\",\n    };\n\n    // Update messages in a single state update\n    setMessages((prev) => [...prev, userMessage, thinkingMessage]);\n    setInput(\"\");\n    setIsLoading(true);\n\n    // Prepare all messages for the API request\n    const apiMessages = [...messages, userMessage].map((msg) => {\n      if (msg.file) {\n        if (msg.file.isText) {\n          // For text files, decode the content before sending\n          const decodedText = decodeURIComponent(atob(msg.file.base64));\n          return {\n            role: msg.role,\n            content: `File contents of ${msg.file.fileName}:\\n\\n${decodedText}\\n\\n${msg.content}`,\n          };\n        } else {\n          // Handle images as before\n          return {\n            role: msg.role,\n            content: [\n              {\n                type: \"image\",\n                source: {\n                  type: \"base64\",\n                  media_type: msg.file.mediaType,\n                  data: msg.file.base64,\n                },\n              },\n              {\n                type: \"text\",\n                text: msg.content,\n              },\n            ],\n          };\n        }\n      }\n      // Handle text-only messages\n      return {\n        role: msg.role,\n        content: msg.content,\n      };\n    });\n\n    const requestBody = {\n      messages: apiMessages,\n      model: selectedModel,\n    };\n\n    try {\n      const response = await fetch(\"/api/finance\", {\n        method: \"POST\",\n        headers: {\n          \"Content-Type\": \"application/json\",\n        },\n        body: JSON.stringify(requestBody),\n      });\n\n      if (!response.ok) {\n        throw new Error(`HTTP error! status: ${response.status}`);\n      }\n\n      const data: APIResponse = await response.json();\n\n      setMessages((prev) => {\n        const newMessages = [...prev];\n        newMessages[newMessages.length - 1] = {\n          id: crypto.randomUUID(),\n          role: \"assistant\",\n          content: data.content,\n          hasToolUse: data.hasToolUse || !!data.toolUse,\n          chartData:\n            data.chartData || (data.toolUse?.input as ChartData) || null,\n        };\n        return newMessages;\n      });\n\n      setCurrentUpload(null);\n    } catch (error) {\n      console.error(\"Submit Error:\", error);\n      setMessages((prev) => {\n        const newMessages = [...prev];\n        newMessages[newMessages.length - 1] = {\n          id: crypto.randomUUID(),\n          role: \"assistant\",\n          content: \"I apologize, but I encountered an error. Please try again.\",\n        };\n        return newMessages;\n      });\n    } finally {\n      setIsLoading(false);\n      setIsScrollLocked(false);\n\n      // Force a final scroll after state updates\n      requestAnimationFrame(() => {\n        messagesEndRef.current?.scrollIntoView({\n          behavior: \"smooth\",\n          block: \"end\",\n        });\n      });\n    }\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n    if (e.key === \"Enter\" && !e.shiftKey) {\n      e.preventDefault();\n      if (input.trim() || currentUpload) {\n        const form = e.currentTarget.form;\n        if (form) {\n          const submitEvent = new Event(\"submit\", {\n            bubbles: true,\n            cancelable: true,\n          });\n          form.dispatchEvent(submitEvent);\n        }\n      }\n    }\n  };\n\n  const handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {\n    const textarea = event.target;\n    setInput(textarea.value);\n    textarea.style.height = \"auto\";\n    textarea.style.height = `${Math.min(textarea.scrollHeight, 300)}px`;\n  };\n\n  return (\n    <div className=\"flex flex-col h-screen\">\n      <TopNavBar\n        features={{\n          showDomainSelector: false,\n          showViewModeSelector: false,\n          showPromptCaching: false,\n        }}\n      />\n\n      <div className=\"flex-1 flex bg-background p-4 pt-0 gap-4 h-[calc(100vh-4rem)]\">\n        {/* Chat Sidebar */}\n        <Card className=\"w-1/3 flex flex-col h-full\">\n          <CardHeader className=\"py-3 px-4\">\n            <div className=\"flex items-center justify-between\">\n              <div className=\"flex items-center space-x-3\">\n                {messages.length > 0 && (\n                  <>\n                    <Avatar className=\"w-8 h-8 border\">\n                      <AvatarImage\n                        src=\"/ant-logo.svg\"\n                        alt=\"AI Assistant Avatar\"\n                      />\n                      <AvatarFallback>AI</AvatarFallback>\n                    </Avatar>\n                    <div>\n                      <CardTitle className=\"text-lg\">\n                        Financial Assistant\n                      </CardTitle>\n                      <CardDescription className=\"text-xs\">\n                        Powered by Claude\n                      </CardDescription>\n                    </div>\n                  </>\n                )}\n              </div>\n\n              <DropdownMenu>\n                <DropdownMenuTrigger asChild>\n                  <Button variant=\"outline\" className=\"h-8 text-sm\">\n                    {models.find((m) => m.id === selectedModel)?.name}\n                    <ChevronDown className=\"ml-2 h-4 w-4\" />\n                  </Button>\n                </DropdownMenuTrigger>\n                <DropdownMenuContent>\n                  {models.map((model) => (\n                    <DropdownMenuItem\n                      key={model.id}\n                      onSelect={() => setSelectedModel(model.id)}\n                    >\n                      {model.name}\n                    </DropdownMenuItem>\n                  ))}\n                </DropdownMenuContent>\n              </DropdownMenu>\n            </div>\n          </CardHeader>\n\n          <CardContent className=\"flex-1 overflow-y-auto p-4 scroll-smooth snap-y snap-mandatory\">\n            {messages.length === 0 ? (\n              <div className=\"flex flex-col items-center justify-center h-full animate-fade-in-up max-w-[95%] mx-auto\">\n                <Avatar className=\"w-10 h-10 mb-4 border\">\n                  <AvatarImage\n                    src=\"/ant-logo.svg\"\n                    alt=\"AI Assistant Avatar\"\n                    width={40}\n                    height={40}\n                  />\n                </Avatar>\n                <h2 className=\"text-xl font-semibold mb-2\">\n                  Financial Assistant\n                </h2>\n                <div className=\"space-y-4 text-base\">\n                  <div className=\"flex items-center gap-3\">\n                    <ChartArea className=\"text-muted-foreground w-6 h-6\" />\n                    <p className=\"text-muted-foreground\">\n                      I can analyze financial data and create visualizations\n                      from your files.\n                    </p>\n                  </div>\n                  <div className=\"flex items-center gap-3\">\n                    <FileInput className=\"text-muted-foreground w-6 h-6\" />\n                    <p className=\"text-muted-foreground\">\n                      Upload CSVs, PDFs, or images and I&apos;ll help you\n                      understand the data.\n                    </p>\n                  </div>\n                  <div className=\"flex items-center gap-3\">\n                    <MessageCircleQuestion className=\"text-muted-foreground w-6 h-6\" />\n                    <p className=\"text-muted-foreground\">\n                      Ask questions about your financial data and I&apos;ll\n                      create insightful charts.\n                    </p>\n                  </div>\n                </div>\n              </div>\n            ) : (\n              <div className=\"space-y-4 min-h-full\">\n                {messages.map((message) => (\n                  <div\n                    key={message.id}\n                    className={`animate-fade-in-up ${\n                      message.content === \"thinking\" ? \"animate-pulse\" : \"\"\n                    }`}\n                  >\n                    <MessageComponent message={message} />\n                  </div>\n                ))}\n                <div ref={messagesEndRef} className=\"h-4\" />{\" \"}\n                {/* Add height to ensure scroll space */}\n              </div>\n            )}\n          </CardContent>\n\n          <CardFooter className=\"p-4 border-t\">\n            <form onSubmit={handleSubmit} className=\"w-full\">\n              <div className=\"flex flex-col space-y-2\">\n                {currentUpload && (\n                  <FilePreview\n                    file={currentUpload}\n                    onRemove={() => setCurrentUpload(null)}\n                  />\n                )}\n                <div className=\"flex items-end space-x-2\">\n                  <div className=\"flex-1 relative\">\n                    <Button\n                      type=\"button\"\n                      variant=\"ghost\"\n                      size=\"icon\"\n                      onClick={() => fileInputRef.current?.click()}\n                      disabled={isLoading || isUploading}\n                      className=\"absolute left-2 top-1/2 -translate-y-1/2 h-8 w-8\"\n                    >\n                      <Paperclip className=\"h-5 w-5\" />\n                    </Button>\n                    <Textarea\n                      value={input}\n                      onChange={handleInputChange}\n                      onKeyDown={handleKeyDown}\n                      placeholder=\"Type your message...\"\n                      disabled={isLoading}\n                      className=\"min-h-[44px] h-[44px] resize-none pl-12 py-3 flex items-center\"\n                      rows={1}\n                    />\n                  </div>\n                  <Button\n                    type=\"submit\"\n                    disabled={isLoading || (!input.trim() && !currentUpload)}\n                    className=\"h-[44px]\"\n                  >\n                    <Send className=\"h-4 w-4\" />\n                  </Button>\n                </div>\n              </div>\n              <input\n                type=\"file\"\n                ref={fileInputRef}\n                className=\"hidden\"\n                onChange={handleFileSelect}\n              />\n            </form>\n          </CardFooter>\n        </Card>\n\n        {/* Content Area */}\n        <Card className=\"flex-1 flex flex-col h-full overflow-hidden\">\n          {messages.some((m) => m.chartData) && (\n            <CardHeader className=\"py-3 px-4 shrink-0\">\n              <CardTitle className=\"text-lg\">\n                Analysis & Visualizations\n              </CardTitle>\n            </CardHeader>\n          )}\n          <CardContent\n            ref={contentRef}\n            className=\"flex-1 overflow-y-auto min-h-0 snap-y snap-mandatory\"\n            onScroll={handleChartScroll}\n          >\n            {messages.some((m) => m.chartData) ? (\n              <div className=\"min-h-full flex flex-col\">\n                {messages.map(\n                  (message, index) =>\n                    message.chartData && (\n                      <div\n                        key={`chart-${index}`}\n                        className=\"w-full min-h-full flex-shrink-0 snap-start snap-always\"\n                        ref={\n                          index ===\n                          messages.filter((m) => m.chartData).length - 1\n                            ? chartEndRef\n                            : null\n                        }\n                      >\n                        <SafeChartRenderer data={message.chartData} />\n                      </div>\n                    ),\n                )}\n              </div>\n            ) : (\n              <div className=\"h-full flex flex-col items-center justify-center text-center\">\n                <div className=\"flex flex-col items-center justify-center gap-4 -translate-y-8\">\n                  <ChartColumnBig className=\"w-8 h-8 text-muted-foreground\" />\n                  <div className=\"space-y-2\">\n                    <CardTitle className=\"text-lg\">\n                      Analysis & Visualizations\n                    </CardTitle>\n                    <CardDescription className=\"text-base\">\n                      Charts and detailed analysis will appear here as you chat\n                    </CardDescription>\n                    <div className=\"flex flex-wrap justify-center gap-2 mt-4\">\n                      <Badge variant=\"outline\">Bar Charts</Badge>\n                      <Badge variant=\"outline\">Area Charts</Badge>\n                      <Badge variant=\"outline\">Linear Charts</Badge>\n                      <Badge variant=\"outline\">Pie Charts</Badge>\n                    </div>\n                  </div>\n                </div>\n              </div>\n            )}\n          </CardContent>\n        </Card>\n      </div>\n      {messages.some((m) => m.chartData) && (\n        <ChartPagination\n          total={messages.filter((m) => m.chartData).length}\n          current={currentChartIndex}\n          onDotClick={scrollToChart}\n        />\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "financial-data-analyst/app/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\nbody {\n    font-family: Arial, Helvetica, sans-serif;\n}\n\n@layer utilities {\n    .text-balance {\n        text-wrap: balance;\n    }\n}\n\n@layer base {\n    :root {\n        --background: 0 0% 100%;\n        --foreground: 222.2 84% 4.9%;\n        --card: 0 0% 100%;\n        --card-foreground: 222.2 84% 4.9%;\n        --popover: 0 0% 100%;\n        --popover-foreground: 222.2 84% 4.9%;\n        --primary: 222.2 47.4% 11.2%;\n        --primary-foreground: 210 40% 98%;\n        --secondary: 210 40% 96.1%;\n        --secondary-foreground: 222.2 47.4% 11.2%;\n        --muted: 210 40% 96.1%;\n        --muted-foreground: 215.4 16.3% 46.9%;\n        --accent: 210 40% 96.1%;\n        --accent-foreground: 222.2 47.4% 11.2%;\n        --destructive: 0 84.2% 60.2%;\n        --destructive-foreground: 210 40% 98%;\n        --border: 214.3 31.8% 91.4%;\n        --input: 214.3 31.8% 91.4%;\n        --ring: 222.2 84% 4.9%;\n        --radius: 0.75rem;\n        --chart-1: 12 76% 61%;\n        --chart-2: 173 58% 39%;\n        --chart-3: 197 37% 24%;\n        --chart-4: 43 74% 66%;\n        --chart-5: 27 87% 67%;\n    }\n    .dark {\n        --background: 222.2 84% 4.9%;\n        --foreground: 210 40% 98%;\n        --card: 222.2 84% 4.9%;\n        --card-foreground: 210 40% 98%;\n        --popover: 222.2 84% 4.9%;\n        --popover-foreground: 210 40% 98%;\n        --primary: 210 40% 98%;\n        --primary-foreground: 222.2 47.4% 11.2%;\n        --secondary: 217.2 32.6% 17.5%;\n        --secondary-foreground: 210 40% 98%;\n        --muted: 217.2 32.6% 17.5%;\n        --muted-foreground: 215 20.2% 65.1%;\n        --accent: 217.2 32.6% 17.5%;\n        --accent-foreground: 210 40% 98%;\n        --destructive: 0 62.8% 30.6%;\n        --destructive-foreground: 210 40% 98%;\n        --border: 217.2 32.6% 17.5%;\n        --input: 217.2 32.6% 17.5%;\n        --ring: 212.7 26.8% 83.9;\n        --chart-1: 220 70% 50%;\n        --chart-2: 160 60% 45%;\n        --chart-3: 30 80% 55%;\n        --chart-4: 280 65% 60%;\n        --chart-5: 340 75% 55%;\n    }\n}\n\n@layer base {\n    * {\n        @apply border-border;\n    }\n    body {\n        @apply bg-background text-foreground;\n    }\n}\n\n@layer utilities {\n    @keyframes fadeInUp {\n        0% {\n            opacity: 0;\n            transform: translateY(10px);\n        }\n        100% {\n            opacity: 1;\n            transform: translateY(0);\n        }\n    }\n\n    @keyframes pulse {\n        0%,\n        100% {\n            opacity: 1;\n        }\n        50% {\n            opacity: 0.7;\n        }\n    }\n\n    .animate-fade-in-up {\n        animation: fadeInUp 0.3s ease-out forwards;\n    }\n\n    .animate-pulse {\n        animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;\n    }\n}\n"
  },
  {
    "path": "financial-data-analyst/app/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport localFont from \"next/font/local\";\nimport { ThemeProvider } from \"@/components/theme-provider\";\nimport { Toaster } from \"@/components/ui/toaster\";\nimport \"./globals.css\";\n\nconst geistSans = localFont({\n  src: \"./fonts/GeistVF.woff\",\n  variable: \"--font-geist-sans\",\n  weight: \"100 900\",\n});\nconst geistMono = localFont({\n  src: \"./fonts/GeistMonoVF.woff\",\n  variable: \"--font-geist-mono\",\n  weight: \"100 900\",\n});\n\nexport const metadata: Metadata = {\n  title: \"Create Next App\",\n  description: \"Generated by create next app\",\n};\n\nexport default function RootLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  return (\n    <html lang=\"en\" suppressHydrationWarning>\n      <body\n        className={`${geistSans.variable} ${geistMono.variable} antialiased`}\n      >\n        <ThemeProvider\n          attribute=\"class\"\n          defaultTheme=\"system\"\n          enableSystem\n          disableTransitionOnChange\n        >\n          {children}\n          <Toaster />\n        </ThemeProvider>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "financial-data-analyst/app/page.tsx",
    "content": "// /app/page.tsx\n\"use client\";\n\nimport React from \"react\";\nimport FinancePage from \"./finance/page\";\n\nexport default function Home() {\n  return <FinancePage />;\n}\n"
  },
  {
    "path": "financial-data-analyst/components/ChartRenderer.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport { TrendingUp, TrendingDown } from \"lucide-react\";\nimport {\n  Area,\n  AreaChart,\n  Bar,\n  BarChart,\n  CartesianGrid,\n  Label,\n  Line,\n  LineChart,\n  Pie,\n  PieChart,\n  XAxis,\n} from \"recharts\";\nimport {\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent,\n} from \"@/components/ui/chart\";\nimport type { ChartData } from \"@/types/chart\";\n\nfunction BarChartComponent({ data }: { data: ChartData }) {\n  const dataKey = Object.keys(data.chartConfig)[0];\n\n  return (\n    <Card>\n      <CardHeader>\n        <CardTitle className=\"text-xl\">{data.config.title}</CardTitle>\n        <CardDescription>{data.config.description}</CardDescription>\n      </CardHeader>\n      <CardContent>\n        <ChartContainer config={data.chartConfig}>\n          <BarChart accessibilityLayer data={data.data}>\n            <CartesianGrid vertical={false} />\n            <XAxis\n              dataKey={data.config.xAxisKey}\n              tickLine={false}\n              tickMargin={10}\n              axisLine={false}\n              tickFormatter={(value) => {\n                return value.length > 20\n                  ? `${value.substring(0, 17)}...`\n                  : value;\n              }}\n            />\n            <ChartTooltip\n              cursor={false}\n              content={<ChartTooltipContent hideLabel />}\n            />\n            <Bar\n              dataKey={dataKey}\n              fill={`var(--color-${dataKey})`}\n              radius={8}\n            />\n          </BarChart>\n        </ChartContainer>\n      </CardContent>\n      <CardFooter className=\"flex-col items-start gap-2 text-sm\">\n        {data.config.trend && (\n          <div className=\"flex gap-2 font-medium leading-none\">\n            Trending {data.config.trend.direction} by{\" \"}\n            {data.config.trend.percentage}% this period{\" \"}\n            {data.config.trend.direction === \"up\" ? (\n              <TrendingUp className=\"h-4 w-4\" />\n            ) : (\n              <TrendingDown className=\"h-4 w-4\" />\n            )}\n          </div>\n        )}\n        {data.config.footer && (\n          <div className=\"leading-none text-muted-foreground\">\n            {data.config.footer}\n          </div>\n        )}\n      </CardFooter>\n    </Card>\n  );\n}\n\nfunction MultiBarChartComponent({ data }: { data: ChartData }) {\n  return (\n    <Card>\n      <CardHeader>\n        <CardTitle className=\"text-xl\">{data.config.title}</CardTitle>\n        <CardDescription>{data.config.description}</CardDescription>\n      </CardHeader>\n      <CardContent>\n        <ChartContainer config={data.chartConfig}>\n          <BarChart accessibilityLayer data={data.data}>\n            <CartesianGrid vertical={false} />\n            <XAxis\n              dataKey={data.config.xAxisKey}\n              tickLine={false}\n              tickMargin={10}\n              axisLine={false}\n              tickFormatter={(value) => {\n                return value.length > 20\n                  ? `${value.substring(0, 17)}...`\n                  : value;\n              }}\n            />\n            <ChartTooltip\n              cursor={false}\n              content={<ChartTooltipContent indicator=\"dashed\" />}\n            />\n            {Object.keys(data.chartConfig).map((key) => (\n              <Bar\n                key={key}\n                dataKey={key}\n                fill={`var(--color-${key})`}\n                radius={4}\n              />\n            ))}\n          </BarChart>\n        </ChartContainer>\n      </CardContent>\n      <CardFooter className=\"flex-col items-start gap-2 text-sm\">\n        {data.config.trend && (\n          <div className=\"flex gap-2 font-medium leading-none\">\n            Trending {data.config.trend.direction} by{\" \"}\n            {data.config.trend.percentage}% this period{\" \"}\n            {data.config.trend.direction === \"up\" ? (\n              <TrendingUp className=\"h-4 w-4\" />\n            ) : (\n              <TrendingDown className=\"h-4 w-4\" />\n            )}\n          </div>\n        )}\n        {data.config.footer && (\n          <div className=\"leading-none text-muted-foreground\">\n            {data.config.footer}\n          </div>\n        )}\n      </CardFooter>\n    </Card>\n  );\n}\n\nfunction LineChartComponent({ data }: { data: ChartData }) {\n  return (\n    <Card>\n      <CardHeader>\n        <CardTitle className=\"text-xl\">{data.config.title}</CardTitle>\n        <CardDescription>{data.config.description}</CardDescription>\n      </CardHeader>\n      <CardContent>\n        <ChartContainer config={data.chartConfig}>\n          <LineChart\n            accessibilityLayer\n            data={data.data}\n            margin={{\n              left: 12,\n              right: 12,\n            }}\n          >\n            <CartesianGrid vertical={false} />\n            <XAxis\n              dataKey={data.config.xAxisKey}\n              tickLine={false}\n              axisLine={false}\n              tickMargin={8}\n              tickFormatter={(value) => {\n                return value.length > 20\n                  ? `${value.substring(0, 17)}...`\n                  : value;\n              }}\n            />\n            <ChartTooltip\n              cursor={false}\n              content={<ChartTooltipContent hideLabel />}\n            />\n            {Object.keys(data.chartConfig).map((key) => (\n              <Line\n                key={key}\n                type=\"natural\"\n                dataKey={key}\n                stroke={`var(--color-${key})`}\n                strokeWidth={2}\n                dot={false}\n              />\n            ))}\n          </LineChart>\n        </ChartContainer>\n      </CardContent>\n      <CardFooter className=\"flex-col items-start gap-2 text-sm\">\n        {data.config.trend && (\n          <div className=\"flex gap-2 font-medium leading-none\">\n            Trending {data.config.trend.direction} by{\" \"}\n            {data.config.trend.percentage}% this period{\" \"}\n            {data.config.trend.direction === \"up\" ? (\n              <TrendingUp className=\"h-4 w-4\" />\n            ) : (\n              <TrendingDown className=\"h-4 w-4\" />\n            )}\n          </div>\n        )}\n        {data.config.footer && (\n          <div className=\"leading-none text-muted-foreground\">\n            {data.config.footer}\n          </div>\n        )}\n      </CardFooter>\n    </Card>\n  );\n}\n\nfunction PieChartComponent({ data }: { data: ChartData }) {\n  const totalValue = React.useMemo(() => {\n    return data.data.reduce((acc, curr) => acc + curr.value, 0);\n  }, [data.data]);\n\n  const chartData = data.data.map((item, index) => {\n    return {\n      ...item,\n      // Use the same color variable pattern as other charts\n      fill: `hsl(var(--chart-${index + 1}))`,\n    };\n  });\n\n  return (\n    <Card className=\"flex flex-col\">\n      <CardHeader className=\"items-center pb-0\">\n        <CardTitle className=\"text-xl\">{data.config.title}</CardTitle>\n        <CardDescription>{data.config.description}</CardDescription>\n      </CardHeader>\n      <CardContent className=\"flex-1 pb-0\">\n        <ChartContainer\n          config={data.chartConfig}\n          className=\"mx-auto aspect-square max-h-[250px]\"\n        >\n          <PieChart>\n            <ChartTooltip\n              cursor={false}\n              content={<ChartTooltipContent hideLabel />}\n            />\n            <Pie\n              data={chartData}\n              dataKey=\"value\"\n              nameKey=\"segment\"\n              innerRadius={60}\n              strokeWidth={5}\n            >\n              <Label\n                content={({ viewBox }) => {\n                  if (viewBox && \"cx\" in viewBox && \"cy\" in viewBox) {\n                    return (\n                      <text\n                        x={viewBox.cx}\n                        y={viewBox.cy}\n                        textAnchor=\"middle\"\n                        dominantBaseline=\"middle\"\n                      >\n                        <tspan\n                          x={viewBox.cx}\n                          y={viewBox.cy}\n                          className=\"fill-foreground text-3xl font-bold\"\n                        >\n                          {totalValue.toLocaleString()}\n                        </tspan>\n                        <tspan\n                          x={viewBox.cx}\n                          y={(viewBox.cy || 0) + 24}\n                          className=\"fill-muted-foreground\"\n                        >\n                          {data.config.totalLabel}\n                        </tspan>\n                      </text>\n                    );\n                  }\n                  return null;\n                }}\n              />\n            </Pie>\n          </PieChart>\n        </ChartContainer>\n      </CardContent>\n      <CardFooter className=\"flex-col gap-2 text-sm\">\n        {data.config.trend && (\n          <div className=\"flex items-center gap-2 font-medium leading-none\">\n            Trending {data.config.trend.direction} by{\" \"}\n            {data.config.trend.percentage}% this period{\" \"}\n            {data.config.trend.direction === \"up\" ? (\n              <TrendingUp className=\"h-4 w-4\" />\n            ) : (\n              <TrendingDown className=\"h-4 w-4\" />\n            )}\n          </div>\n        )}\n        {data.config.footer && (\n          <div className=\"leading-none text-muted-foreground\">\n            {data.config.footer}\n          </div>\n        )}\n      </CardFooter>\n    </Card>\n  );\n}\n\nfunction AreaChartComponent({\n  data,\n  stacked,\n}: {\n  data: ChartData;\n  stacked?: boolean;\n}) {\n  return (\n    <Card>\n      <CardHeader>\n        <CardTitle className=\"text-xl\">{data.config.title}</CardTitle>\n        <CardDescription>{data.config.description}</CardDescription>\n      </CardHeader>\n      <CardContent>\n        <ChartContainer config={data.chartConfig}>\n          <AreaChart\n            accessibilityLayer\n            data={data.data}\n            margin={{\n              left: 12,\n              right: 12,\n            }}\n          >\n            <CartesianGrid vertical={false} />\n            <XAxis\n              dataKey={data.config.xAxisKey}\n              tickLine={false}\n              axisLine={false}\n              tickMargin={8}\n              tickFormatter={(value) => {\n                return value.length > 20\n                  ? `${value.substring(0, 17)}...`\n                  : value;\n              }}\n            />\n            <ChartTooltip\n              cursor={false}\n              content={\n                <ChartTooltipContent indicator={stacked ? \"dot\" : \"line\"} />\n              }\n            />\n            {Object.keys(data.chartConfig).map((key) => (\n              <Area\n                key={key}\n                type=\"natural\"\n                dataKey={key}\n                fill={`var(--color-${key})`}\n                fillOpacity={0.4}\n                stroke={`var(--color-${key})`}\n                stackId={stacked ? \"a\" : undefined}\n              />\n            ))}\n          </AreaChart>\n        </ChartContainer>\n      </CardContent>\n      <CardFooter>\n        <div className=\"flex w-full items-start gap-2 text-sm\">\n          <div className=\"grid gap-2\">\n            {data.config.trend && (\n              <div className=\"flex items-center gap-2 font-medium leading-none\">\n                Trending {data.config.trend.direction} by{\" \"}\n                {data.config.trend.percentage}% this period{\" \"}\n                {data.config.trend.direction === \"up\" ? (\n                  <TrendingUp className=\"h-4 w-4\" />\n                ) : (\n                  <TrendingDown className=\"h-4 w-4\" />\n                )}\n              </div>\n            )}\n            {data.config.footer && (\n              <div className=\"leading-none text-muted-foreground\">\n                {data.config.footer}\n              </div>\n            )}\n          </div>\n        </div>\n      </CardFooter>\n    </Card>\n  );\n}\n\nexport function ChartRenderer({ data }: { data: ChartData }) {\n  switch (data.chartType) {\n    case \"bar\":\n      return <BarChartComponent data={data} />;\n    case \"multiBar\":\n      return <MultiBarChartComponent data={data} />;\n    case \"line\":\n      return <LineChartComponent data={data} />;\n    case \"pie\":\n      return <PieChartComponent data={data} />;\n    case \"area\":\n      return <AreaChartComponent data={data} />;\n    case \"stackedArea\":\n      return <AreaChartComponent data={data} stacked />;\n    default:\n      return null;\n  }\n}\n"
  },
  {
    "path": "financial-data-analyst/components/FilePreview.tsx",
    "content": "import React from \"react\";\nimport { X, FileText } from \"lucide-react\";\nimport { Badge } from \"@/components/ui/badge\";\nimport Image from \"next/image\";\n\ninterface FilePreviewProps {\n  file: {\n    base64: string;\n    fileName: string;\n    mediaType: string;\n    isText?: boolean;\n  };\n  onRemove?: () => void;\n  size?: \"small\" | \"large\";\n}\n\nconst FilePreview: React.FC<FilePreviewProps> = ({\n  file,\n  onRemove,\n  size = \"large\",\n}) => {\n  const isImage = file.mediaType.startsWith(\"image/\");\n  const fileExtension = file.fileName.split(\".\").pop()?.toLowerCase() || \"\";\n\n  const truncatedName =\n    file.fileName.length > 7\n      ? `${file.fileName.slice(0, 7)}...${file.fileName.slice(\n          file.fileName.lastIndexOf(\".\"),\n        )}`\n      : file.fileName;\n\n  const imageUrl = isImage\n    ? `data:${file.mediaType};base64,${file.base64}`\n    : \"\";\n\n  if (size === \"small\") {\n    return (\n      <Badge variant=\"secondary\" className=\"gap-2 py-1 px-3\">\n        {isImage ? (\n          <div className=\"relative w-4 h-4\">\n            <Image\n              src={imageUrl}\n              alt={file.fileName}\n              className=\"object-cover rounded\"\n              fill\n              sizes=\"16px\"\n              unoptimized\n            />\n          </div>\n        ) : (\n          <FileText className=\"w-4 h-4\" />\n        )}\n        <span className=\"text-xs\">{truncatedName}</span>\n      </Badge>\n    );\n  }\n\n  return (\n    <div className=\"relative inline-flex items-center rounded-lg border bg-card text-card-foreground shadow-sm w-16 h-16\">\n      {isImage ? (\n        <div className=\"relative w-full h-full\">\n          <Image\n            src={imageUrl}\n            alt={file.fileName}\n            className=\"object-cover rounded-lg\"\n            fill\n            sizes=\"64px\"\n            unoptimized\n          />\n        </div>\n      ) : (\n        <div className=\"w-full h-full flex flex-col items-center justify-center bg-muted rounded-lg\">\n          <FileText className=\"w-6 h-6 mb-1\" />\n          <span className=\"text-xs font-medium uppercase\">{fileExtension}</span>\n        </div>\n      )}\n      {onRemove && (\n        <button\n          onClick={(e) => {\n            e.preventDefault();\n            onRemove();\n          }}\n          className=\"absolute -top-2 -right-2 rounded-full bg-destructive p-1 text-destructive-foreground hover:bg-destructive/90\"\n        >\n          <X className=\"h-3 w-3\" />\n        </button>\n      )}\n    </div>\n  );\n};\n\nexport default FilePreview;\n"
  },
  {
    "path": "financial-data-analyst/components/TopNavBar.tsx",
    "content": "\"use client\";\nimport React, { useState, useEffect } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport Image from \"next/image\";\nimport { Moon, Sun } from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\n\n// Add this interface to define the props structure\ninterface TopNavBarProps {\n  features?: {\n    showDomainSelector?: boolean;\n    showViewModeSelector?: boolean;\n    showPromptCaching?: boolean;\n  };\n}\n\n// Change this line to include the props type\nconst TopNavBar: React.FC<TopNavBarProps> = ({ features = {} }) => {\n  const { theme, setTheme } = useTheme();\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  if (!mounted) {\n    return null;\n  }\n\n  return (\n    <div className=\"flex items-center justify-between p-4\">\n      <div className=\"font-bold text-xl flex gap-2 items-center\">\n        <Image\n          src={theme === \"dark\" ? \"/wordmark-dark.svg\" : \"/wordmark.svg\"}\n          alt=\"Company Wordmark\"\n          width={112}\n          height={20}\n        />\n      </div>\n      <div className=\"flex items-center gap-2\">\n        <DropdownMenu>\n          <DropdownMenuTrigger asChild>\n            <Button variant=\"outline\" size=\"icon\">\n              <Sun className=\"h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0\" />\n              <Moon className=\"absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100\" />\n              <span className=\"sr-only\">Toggle theme</span>\n            </Button>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent align=\"end\">\n            <DropdownMenuItem onClick={() => setTheme(\"light\")}>\n              Light\n            </DropdownMenuItem>\n            <DropdownMenuItem onClick={() => setTheme(\"dark\")}>\n              Dark\n            </DropdownMenuItem>\n            <DropdownMenuItem onClick={() => setTheme(\"system\")}>\n              System\n            </DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n    </div>\n  );\n};\n\nexport default TopNavBar;\n"
  },
  {
    "path": "financial-data-analyst/components/theme-provider.tsx",
    "content": "// components/theme-provider.tsx\n\"use client\";\n\nimport * as React from \"react\";\nimport { ThemeProvider as NextThemesProvider } from \"next-themes\";\nimport { type ThemeProviderProps } from \"next-themes/dist/types\";\n\nexport function ThemeProvider({ children, ...props }: ThemeProviderProps) {\n  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;\n}\n"
  },
  {
    "path": "financial-data-analyst/components/ui/avatar.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Avatar = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full\",\n      className\n    )}\n    {...props}\n  />\n))\nAvatar.displayName = AvatarPrimitive.Root.displayName\n\nconst AvatarImage = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Image>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Image\n    ref={ref}\n    className={cn(\"aspect-square h-full w-full\", className)}\n    {...props}\n  />\n))\nAvatarImage.displayName = AvatarPrimitive.Image.displayName\n\nconst AvatarFallback = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Fallback>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Fallback\n    ref={ref}\n    className={cn(\n      \"flex h-full w-full items-center justify-center rounded-full bg-muted\",\n      className\n    )}\n    {...props}\n  />\n))\nAvatarFallback.displayName = AvatarPrimitive.Fallback.displayName\n\nexport { Avatar, AvatarImage, AvatarFallback }\n"
  },
  {
    "path": "financial-data-analyst/components/ui/badge.tsx",
    "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst badgeVariants = cva(\n  \"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80\",\n        secondary:\n          \"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        destructive:\n          \"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80\",\n        outline: \"text-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n)\n\nexport interface BadgeProps\n  extends React.HTMLAttributes<HTMLDivElement>,\n    VariantProps<typeof badgeVariants> {}\n\nfunction Badge({ className, variant, ...props }: BadgeProps) {\n  return (\n    <div className={cn(badgeVariants({ variant }), className)} {...props} />\n  )\n}\n\nexport { Badge, badgeVariants }\n"
  },
  {
    "path": "financial-data-analyst/components/ui/button.tsx",
    "content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"bg-primary text-primary-foreground shadow hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90\",\n        outline:\n          \"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground\",\n        secondary:\n          \"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-9 px-4 py-2\",\n        sm: \"h-8 rounded-md px-3 text-xs\",\n        lg: \"h-10 rounded-md px-8\",\n        icon: \"h-9 w-9\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n)\n\nexport interface ButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : \"button\"\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      />\n    )\n  }\n)\nButton.displayName = \"Button\"\n\nexport { Button, buttonVariants }\n"
  },
  {
    "path": "financial-data-analyst/components/ui/card.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Card = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\n      \"rounded-xl border bg-card text-card-foreground shadow\",\n      className\n    )}\n    {...props}\n  />\n))\nCard.displayName = \"Card\"\n\nconst CardHeader = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex flex-col space-y-1.5 p-6\", className)}\n    {...props}\n  />\n))\nCardHeader.displayName = \"CardHeader\"\n\nconst CardTitle = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n  <h3\n    ref={ref}\n    className={cn(\"font-semibold leading-none tracking-tight\", className)}\n    {...props}\n  />\n))\nCardTitle.displayName = \"CardTitle\"\n\nconst CardDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n  <p\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n))\nCardDescription.displayName = \"CardDescription\"\n\nconst CardContent = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div ref={ref} className={cn(\"p-6 pt-0\", className)} {...props} />\n))\nCardContent.displayName = \"CardContent\"\n\nconst CardFooter = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex items-center p-6 pt-0\", className)}\n    {...props}\n  />\n))\nCardFooter.displayName = \"CardFooter\"\n\nexport { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }\n"
  },
  {
    "path": "financial-data-analyst/components/ui/chart.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as RechartsPrimitive from \"recharts\"\nimport {\n  NameType,\n  Payload,\n  ValueType,\n} from \"recharts/types/component/DefaultTooltipContent\"\n\nimport { cn } from \"@/lib/utils\"\n\n// Format: { THEME_NAME: CSS_SELECTOR }\nconst THEMES = { light: \"\", dark: \".dark\" } as const\n\nexport type ChartConfig = {\n  [k in string]: {\n    label?: React.ReactNode\n    icon?: React.ComponentType\n  } & (\n    | { color?: string; theme?: never }\n    | { color?: never; theme: Record<keyof typeof THEMES, string> }\n  )\n}\n\ntype ChartContextProps = {\n  config: ChartConfig\n}\n\nconst ChartContext = React.createContext<ChartContextProps | null>(null)\n\nfunction useChart() {\n  const context = React.useContext(ChartContext)\n\n  if (!context) {\n    throw new Error(\"useChart must be used within a <ChartContainer />\")\n  }\n\n  return context\n}\n\nconst ChartContainer = React.forwardRef<\n  HTMLDivElement,\n  React.ComponentProps<\"div\"> & {\n    config: ChartConfig\n    children: React.ComponentProps<\n      typeof RechartsPrimitive.ResponsiveContainer\n    >[\"children\"]\n  }\n>(({ id, className, children, config, ...props }, ref) => {\n  const uniqueId = React.useId()\n  const chartId = `chart-${id || uniqueId.replace(/:/g, \"\")}`\n\n  return (\n    <ChartContext.Provider value={{ config }}>\n      <div\n        data-chart={chartId}\n        ref={ref}\n        className={cn(\n          \"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none\",\n          className\n        )}\n        {...props}\n      >\n        <ChartStyle id={chartId} config={config} />\n        <RechartsPrimitive.ResponsiveContainer>\n          {children}\n        </RechartsPrimitive.ResponsiveContainer>\n      </div>\n    </ChartContext.Provider>\n  )\n})\nChartContainer.displayName = \"Chart\"\n\nconst ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {\n  const colorConfig = Object.entries(config).filter(\n    ([_, config]) => config.theme || config.color\n  )\n\n  if (!colorConfig.length) {\n    return null\n  }\n\n  return (\n    <style\n      dangerouslySetInnerHTML={{\n        __html: Object.entries(THEMES)\n          .map(\n            ([theme, prefix]) => `\n${prefix} [data-chart=${id}] {\n${colorConfig\n  .map(([key, itemConfig]) => {\n    const color =\n      itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||\n      itemConfig.color\n    return color ? `  --color-${key}: ${color};` : null\n  })\n  .join(\"\\n\")}\n}\n`\n          )\n          .join(\"\\n\"),\n      }}\n    />\n  )\n}\n\nconst ChartTooltip = RechartsPrimitive.Tooltip\n\nconst ChartTooltipContent = React.forwardRef<\n  HTMLDivElement,\n  React.ComponentProps<typeof RechartsPrimitive.Tooltip> &\n    React.ComponentProps<\"div\"> & {\n      hideLabel?: boolean\n      hideIndicator?: boolean\n      indicator?: \"line\" | \"dot\" | \"dashed\"\n      nameKey?: string\n      labelKey?: string\n    }\n>(\n  (\n    {\n      active,\n      payload,\n      className,\n      indicator = \"dot\",\n      hideLabel = false,\n      hideIndicator = false,\n      label,\n      labelFormatter,\n      labelClassName,\n      formatter,\n      color,\n      nameKey,\n      labelKey,\n    },\n    ref\n  ) => {\n    const { config } = useChart()\n\n    const tooltipLabel = React.useMemo(() => {\n      if (hideLabel || !payload?.length) {\n        return null\n      }\n\n      const [item] = payload\n      const key = `${labelKey || item.dataKey || item.name || \"value\"}`\n      const itemConfig = getPayloadConfigFromPayload(config, item, key)\n      const value =\n        !labelKey && typeof label === \"string\"\n          ? config[label as keyof typeof config]?.label || label\n          : itemConfig?.label\n\n      if (labelFormatter) {\n        return (\n          <div className={cn(\"font-medium\", labelClassName)}>\n            {labelFormatter(value, payload)}\n          </div>\n        )\n      }\n\n      if (!value) {\n        return null\n      }\n\n      return <div className={cn(\"font-medium\", labelClassName)}>{value}</div>\n    }, [\n      label,\n      labelFormatter,\n      payload,\n      hideLabel,\n      labelClassName,\n      config,\n      labelKey,\n    ])\n\n    if (!active || !payload?.length) {\n      return null\n    }\n\n    const nestLabel = payload.length === 1 && indicator !== \"dot\"\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          \"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl\",\n          className\n        )}\n      >\n        {!nestLabel ? tooltipLabel : null}\n        <div className=\"grid gap-1.5\">\n          {payload.map((item, index) => {\n            const key = `${nameKey || item.name || item.dataKey || \"value\"}`\n            const itemConfig = getPayloadConfigFromPayload(config, item, key)\n            const indicatorColor = color || item.payload.fill || item.color\n\n            return (\n              <div\n                key={item.dataKey}\n                className={cn(\n                  \"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground\",\n                  indicator === \"dot\" && \"items-center\"\n                )}\n              >\n                {formatter && item?.value !== undefined && item.name ? (\n                  formatter(item.value, item.name, item, index, item.payload)\n                ) : (\n                  <>\n                    {itemConfig?.icon ? (\n                      <itemConfig.icon />\n                    ) : (\n                      !hideIndicator && (\n                        <div\n                          className={cn(\n                            \"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]\",\n                            {\n                              \"h-2.5 w-2.5\": indicator === \"dot\",\n                              \"w-1\": indicator === \"line\",\n                              \"w-0 border-[1.5px] border-dashed bg-transparent\":\n                                indicator === \"dashed\",\n                              \"my-0.5\": nestLabel && indicator === \"dashed\",\n                            }\n                          )}\n                          style={\n                            {\n                              \"--color-bg\": indicatorColor,\n                              \"--color-border\": indicatorColor,\n                            } as React.CSSProperties\n                          }\n                        />\n                      )\n                    )}\n                    <div\n                      className={cn(\n                        \"flex flex-1 justify-between leading-none\",\n                        nestLabel ? \"items-end\" : \"items-center\"\n                      )}\n                    >\n                      <div className=\"grid gap-1.5\">\n                        {nestLabel ? tooltipLabel : null}\n                        <span className=\"text-muted-foreground\">\n                          {itemConfig?.label || item.name}\n                        </span>\n                      </div>\n                      {item.value && (\n                        <span className=\"font-mono font-medium tabular-nums text-foreground\">\n                          {item.value.toLocaleString()}\n                        </span>\n                      )}\n                    </div>\n                  </>\n                )}\n              </div>\n            )\n          })}\n        </div>\n      </div>\n    )\n  }\n)\nChartTooltipContent.displayName = \"ChartTooltip\"\n\nconst ChartLegend = RechartsPrimitive.Legend\n\nconst ChartLegendContent = React.forwardRef<\n  HTMLDivElement,\n  React.ComponentProps<\"div\"> &\n    Pick<RechartsPrimitive.LegendProps, \"payload\" | \"verticalAlign\"> & {\n      hideIcon?: boolean\n      nameKey?: string\n    }\n>(\n  (\n    { className, hideIcon = false, payload, verticalAlign = \"bottom\", nameKey },\n    ref\n  ) => {\n    const { config } = useChart()\n\n    if (!payload?.length) {\n      return null\n    }\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          \"flex items-center justify-center gap-4\",\n          verticalAlign === \"top\" ? \"pb-3\" : \"pt-3\",\n          className\n        )}\n      >\n        {payload.map((item) => {\n          const key = `${nameKey || item.dataKey || \"value\"}`\n          const itemConfig = getPayloadConfigFromPayload(config, item, key)\n\n          return (\n            <div\n              key={item.value}\n              className={cn(\n                \"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground\"\n              )}\n            >\n              {itemConfig?.icon && !hideIcon ? (\n                <itemConfig.icon />\n              ) : (\n                <div\n                  className=\"h-2 w-2 shrink-0 rounded-[2px]\"\n                  style={{\n                    backgroundColor: item.color,\n                  }}\n                />\n              )}\n              {itemConfig?.label}\n            </div>\n          )\n        })}\n      </div>\n    )\n  }\n)\nChartLegendContent.displayName = \"ChartLegend\"\n\n// Helper to extract item config from a payload.\nfunction getPayloadConfigFromPayload(\n  config: ChartConfig,\n  payload: unknown,\n  key: string\n) {\n  if (typeof payload !== \"object\" || payload === null) {\n    return undefined\n  }\n\n  const payloadPayload =\n    \"payload\" in payload &&\n    typeof payload.payload === \"object\" &&\n    payload.payload !== null\n      ? payload.payload\n      : undefined\n\n  let configLabelKey: string = key\n\n  if (\n    key in payload &&\n    typeof payload[key as keyof typeof payload] === \"string\"\n  ) {\n    configLabelKey = payload[key as keyof typeof payload] as string\n  } else if (\n    payloadPayload &&\n    key in payloadPayload &&\n    typeof payloadPayload[key as keyof typeof payloadPayload] === \"string\"\n  ) {\n    configLabelKey = payloadPayload[\n      key as keyof typeof payloadPayload\n    ] as string\n  }\n\n  return configLabelKey in config\n    ? config[configLabelKey]\n    : config[key as keyof typeof config]\n}\n\nexport {\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent,\n  ChartLegend,\n  ChartLegendContent,\n  ChartStyle,\n}\n"
  },
  {
    "path": "financial-data-analyst/components/ui/dropdown-menu.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\nimport {\n  CheckIcon,\n  ChevronRightIcon,\n  DotFilledIcon,\n} from \"@radix-ui/react-icons\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst DropdownMenu = DropdownMenuPrimitive.Root\n\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger\n\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group\n\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal\n\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub\n\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\n    inset?: boolean\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      \"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRightIcon className=\"ml-auto h-4 w-4\" />\n  </DropdownMenuPrimitive.SubTrigger>\n))\nDropdownMenuSubTrigger.displayName =\n  DropdownMenuPrimitive.SubTrigger.displayName\n\nconst DropdownMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuSubContent.displayName =\n  DropdownMenuPrimitive.SubContent.displayName\n\nconst DropdownMenuContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <DropdownMenuPrimitive.Portal>\n    <DropdownMenuPrimitive.Content\n      ref={ref}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md\",\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className\n      )}\n      {...props}\n    />\n  </DropdownMenuPrimitive.Portal>\n))\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName\n\nconst DropdownMenuItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\n    inset?: boolean\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <DropdownMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <CheckIcon className=\"h-4 w-4\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.CheckboxItem>\n))\nDropdownMenuCheckboxItem.displayName =\n  DropdownMenuPrimitive.CheckboxItem.displayName\n\nconst DropdownMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <DotFilledIcon className=\"h-4 w-4 fill-current\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.RadioItem>\n))\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName\n\nconst DropdownMenuLabel = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\n    inset?: boolean\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Label\n    ref={ref}\n    className={cn(\n      \"px-2 py-1.5 text-sm font-semibold\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  />\n))\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName\n\nconst DropdownMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n))\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName\n\nconst DropdownMenuShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\"ml-auto text-xs tracking-widest opacity-60\", className)}\n      {...props}\n    />\n  )\n}\nDropdownMenuShortcut.displayName = \"DropdownMenuShortcut\"\n\nexport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuGroup,\n  DropdownMenuPortal,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuRadioGroup,\n}\n"
  },
  {
    "path": "financial-data-analyst/components/ui/textarea.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface TextareaProps\n  extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n  ({ className, ...props }, ref) => {\n    return (\n      <textarea\n        className={cn(\n          \"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50\",\n          className\n        )}\n        ref={ref}\n        {...props}\n      />\n    )\n  }\n)\nTextarea.displayName = \"Textarea\"\n\nexport { Textarea }\n"
  },
  {
    "path": "financial-data-analyst/components/ui/toast.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Cross2Icon } from \"@radix-ui/react-icons\"\nimport * as ToastPrimitives from \"@radix-ui/react-toast\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst ToastProvider = ToastPrimitives.Provider\n\nconst ToastViewport = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Viewport>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Viewport\n    ref={ref}\n    className={cn(\n      \"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]\",\n      className\n    )}\n    {...props}\n  />\n))\nToastViewport.displayName = ToastPrimitives.Viewport.displayName\n\nconst toastVariants = cva(\n  \"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full\",\n  {\n    variants: {\n      variant: {\n        default: \"border bg-background text-foreground\",\n        destructive:\n          \"destructive group border-destructive bg-destructive text-destructive-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n)\n\nconst Toast = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &\n    VariantProps<typeof toastVariants>\n>(({ className, variant, ...props }, ref) => {\n  return (\n    <ToastPrimitives.Root\n      ref={ref}\n      className={cn(toastVariants({ variant }), className)}\n      {...props}\n    />\n  )\n})\nToast.displayName = ToastPrimitives.Root.displayName\n\nconst ToastAction = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Action>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Action\n    ref={ref}\n    className={cn(\n      \"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive\",\n      className\n    )}\n    {...props}\n  />\n))\nToastAction.displayName = ToastPrimitives.Action.displayName\n\nconst ToastClose = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Close>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Close\n    ref={ref}\n    className={cn(\n      \"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600\",\n      className\n    )}\n    toast-close=\"\"\n    {...props}\n  >\n    <Cross2Icon className=\"h-4 w-4\" />\n  </ToastPrimitives.Close>\n))\nToastClose.displayName = ToastPrimitives.Close.displayName\n\nconst ToastTitle = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Title>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Title\n    ref={ref}\n    className={cn(\"text-sm font-semibold [&+div]:text-xs\", className)}\n    {...props}\n  />\n))\nToastTitle.displayName = ToastPrimitives.Title.displayName\n\nconst ToastDescription = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Description>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Description\n    ref={ref}\n    className={cn(\"text-sm opacity-90\", className)}\n    {...props}\n  />\n))\nToastDescription.displayName = ToastPrimitives.Description.displayName\n\ntype ToastProps = React.ComponentPropsWithoutRef<typeof Toast>\n\ntype ToastActionElement = React.ReactElement<typeof ToastAction>\n\nexport {\n  type ToastProps,\n  type ToastActionElement,\n  ToastProvider,\n  ToastViewport,\n  Toast,\n  ToastTitle,\n  ToastDescription,\n  ToastClose,\n  ToastAction,\n}\n"
  },
  {
    "path": "financial-data-analyst/components/ui/toaster.tsx",
    "content": "\"use client\"\n\nimport { useToast } from \"@/hooks/use-toast\"\nimport {\n  Toast,\n  ToastClose,\n  ToastDescription,\n  ToastProvider,\n  ToastTitle,\n  ToastViewport,\n} from \"@/components/ui/toast\"\n\nexport function Toaster() {\n  const { toasts } = useToast()\n\n  return (\n    <ToastProvider>\n      {toasts.map(function ({ id, title, description, action, ...props }) {\n        return (\n          <Toast key={id} {...props}>\n            <div className=\"grid gap-1\">\n              {title && <ToastTitle>{title}</ToastTitle>}\n              {description && (\n                <ToastDescription>{description}</ToastDescription>\n              )}\n            </div>\n            {action}\n            <ToastClose />\n          </Toast>\n        )\n      })}\n      <ToastViewport />\n    </ToastProvider>\n  )\n}\n"
  },
  {
    "path": "financial-data-analyst/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.ts\",\n    \"css\": \"app/globals.css\",\n    \"baseColor\": \"stone\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  }\n}"
  },
  {
    "path": "financial-data-analyst/hooks/use-toast.ts",
    "content": "\"use client\"\n\n// Inspired by react-hot-toast library\nimport * as React from \"react\"\n\nimport type {\n  ToastActionElement,\n  ToastProps,\n} from \"@/components/ui/toast\"\n\nconst TOAST_LIMIT = 1\nconst TOAST_REMOVE_DELAY = 1000000\n\ntype ToasterToast = ToastProps & {\n  id: string\n  title?: React.ReactNode\n  description?: React.ReactNode\n  action?: ToastActionElement\n}\n\nconst actionTypes = {\n  ADD_TOAST: \"ADD_TOAST\",\n  UPDATE_TOAST: \"UPDATE_TOAST\",\n  DISMISS_TOAST: \"DISMISS_TOAST\",\n  REMOVE_TOAST: \"REMOVE_TOAST\",\n} as const\n\nlet count = 0\n\nfunction genId() {\n  count = (count + 1) % Number.MAX_SAFE_INTEGER\n  return count.toString()\n}\n\ntype ActionType = typeof actionTypes\n\ntype Action =\n  | {\n      type: ActionType[\"ADD_TOAST\"]\n      toast: ToasterToast\n    }\n  | {\n      type: ActionType[\"UPDATE_TOAST\"]\n      toast: Partial<ToasterToast>\n    }\n  | {\n      type: ActionType[\"DISMISS_TOAST\"]\n      toastId?: ToasterToast[\"id\"]\n    }\n  | {\n      type: ActionType[\"REMOVE_TOAST\"]\n      toastId?: ToasterToast[\"id\"]\n    }\n\ninterface State {\n  toasts: ToasterToast[]\n}\n\nconst toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()\n\nconst addToRemoveQueue = (toastId: string) => {\n  if (toastTimeouts.has(toastId)) {\n    return\n  }\n\n  const timeout = setTimeout(() => {\n    toastTimeouts.delete(toastId)\n    dispatch({\n      type: \"REMOVE_TOAST\",\n      toastId: toastId,\n    })\n  }, TOAST_REMOVE_DELAY)\n\n  toastTimeouts.set(toastId, timeout)\n}\n\nexport const reducer = (state: State, action: Action): State => {\n  switch (action.type) {\n    case \"ADD_TOAST\":\n      return {\n        ...state,\n        toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),\n      }\n\n    case \"UPDATE_TOAST\":\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === action.toast.id ? { ...t, ...action.toast } : t\n        ),\n      }\n\n    case \"DISMISS_TOAST\": {\n      const { toastId } = action\n\n      // ! Side effects ! - This could be extracted into a dismissToast() action,\n      // but I'll keep it here for simplicity\n      if (toastId) {\n        addToRemoveQueue(toastId)\n      } else {\n        state.toasts.forEach((toast) => {\n          addToRemoveQueue(toast.id)\n        })\n      }\n\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === toastId || toastId === undefined\n            ? {\n                ...t,\n                open: false,\n              }\n            : t\n        ),\n      }\n    }\n    case \"REMOVE_TOAST\":\n      if (action.toastId === undefined) {\n        return {\n          ...state,\n          toasts: [],\n        }\n      }\n      return {\n        ...state,\n        toasts: state.toasts.filter((t) => t.id !== action.toastId),\n      }\n  }\n}\n\nconst listeners: Array<(state: State) => void> = []\n\nlet memoryState: State = { toasts: [] }\n\nfunction dispatch(action: Action) {\n  memoryState = reducer(memoryState, action)\n  listeners.forEach((listener) => {\n    listener(memoryState)\n  })\n}\n\ntype Toast = Omit<ToasterToast, \"id\">\n\nfunction toast({ ...props }: Toast) {\n  const id = genId()\n\n  const update = (props: ToasterToast) =>\n    dispatch({\n      type: \"UPDATE_TOAST\",\n      toast: { ...props, id },\n    })\n  const dismiss = () => dispatch({ type: \"DISMISS_TOAST\", toastId: id })\n\n  dispatch({\n    type: \"ADD_TOAST\",\n    toast: {\n      ...props,\n      id,\n      open: true,\n      onOpenChange: (open) => {\n        if (!open) dismiss()\n      },\n    },\n  })\n\n  return {\n    id: id,\n    dismiss,\n    update,\n  }\n}\n\nfunction useToast() {\n  const [state, setState] = React.useState<State>(memoryState)\n\n  React.useEffect(() => {\n    listeners.push(setState)\n    return () => {\n      const index = listeners.indexOf(setState)\n      if (index > -1) {\n        listeners.splice(index, 1)\n      }\n    }\n  }, [state])\n\n  return {\n    ...state,\n    toast,\n    dismiss: (toastId?: string) => dispatch({ type: \"DISMISS_TOAST\", toastId }),\n  }\n}\n\nexport { useToast, toast }\n"
  },
  {
    "path": "financial-data-analyst/lib/utils.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"
  },
  {
    "path": "financial-data-analyst/next.config.mjs",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {};\n\nexport default nextConfig;\n"
  },
  {
    "path": "financial-data-analyst/package.json",
    "content": "{\n  \"name\": \"financial-assistant\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\"\n  },\n  \"dependencies\": {\n    \"@anthropic-ai/sdk\": \"^0.29.0\",\n    \"@radix-ui/react-avatar\": \"^1.1.1\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.1.2\",\n    \"@radix-ui/react-icons\": \"^1.3.0\",\n    \"@radix-ui/react-slot\": \"^1.1.0\",\n    \"@radix-ui/react-toast\": \"^1.2.2\",\n    \"class-variance-authority\": \"^0.7.0\",\n    \"clsx\": \"^2.1.1\",\n    \"lucide-react\": \"^0.452.0\",\n    \"next\": \"14.2.15\",\n    \"next-themes\": \"^0.3.0\",\n    \"pdfjs-dist\": \"^4.7.76\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"recharts\": \"^2.13.0\",\n    \"tailwind-merge\": \"^2.5.3\",\n    \"tailwindcss-animate\": \"^1.0.7\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^20\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"eslint\": \"^8\",\n    \"eslint-config-next\": \"14.2.15\",\n    \"postcss\": \"^8\",\n    \"tailwindcss\": \"^3.4.1\",\n    \"typescript\": \"^5\"\n  }\n}\n"
  },
  {
    "path": "financial-data-analyst/postcss.config.mjs",
    "content": "/** @type {import('postcss-load-config').Config} */\nconst config = {\n  plugins: {\n    tailwindcss: {},\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "financial-data-analyst/tailwind.config.ts",
    "content": "import type { Config } from \"tailwindcss\";\n\nconst config: Config = {\n    darkMode: [\"class\"],\n    content: [\n    \"./pages/**/*.{js,ts,jsx,tsx,mdx}\",\n    \"./components/**/*.{js,ts,jsx,tsx,mdx}\",\n    \"./app/**/*.{js,ts,jsx,tsx,mdx}\",\n  ],\n  theme: {\n  \textend: {\n  \t\tcolors: {\n  \t\t\tbackground: 'hsl(var(--background))',\n  \t\t\tforeground: 'hsl(var(--foreground))',\n  \t\t\tcard: {\n  \t\t\t\tDEFAULT: 'hsl(var(--card))',\n  \t\t\t\tforeground: 'hsl(var(--card-foreground))'\n  \t\t\t},\n  \t\t\tpopover: {\n  \t\t\t\tDEFAULT: 'hsl(var(--popover))',\n  \t\t\t\tforeground: 'hsl(var(--popover-foreground))'\n  \t\t\t},\n  \t\t\tprimary: {\n  \t\t\t\tDEFAULT: 'hsl(var(--primary))',\n  \t\t\t\tforeground: 'hsl(var(--primary-foreground))'\n  \t\t\t},\n  \t\t\tsecondary: {\n  \t\t\t\tDEFAULT: 'hsl(var(--secondary))',\n  \t\t\t\tforeground: 'hsl(var(--secondary-foreground))'\n  \t\t\t},\n  \t\t\tmuted: {\n  \t\t\t\tDEFAULT: 'hsl(var(--muted))',\n  \t\t\t\tforeground: 'hsl(var(--muted-foreground))'\n  \t\t\t},\n  \t\t\taccent: {\n  \t\t\t\tDEFAULT: 'hsl(var(--accent))',\n  \t\t\t\tforeground: 'hsl(var(--accent-foreground))'\n  \t\t\t},\n  \t\t\tdestructive: {\n  \t\t\t\tDEFAULT: 'hsl(var(--destructive))',\n  \t\t\t\tforeground: 'hsl(var(--destructive-foreground))'\n  \t\t\t},\n  \t\t\tborder: 'hsl(var(--border))',\n  \t\t\tinput: 'hsl(var(--input))',\n  \t\t\tring: 'hsl(var(--ring))',\n  \t\t\tchart: {\n  \t\t\t\t'1': 'hsl(var(--chart-1))',\n  \t\t\t\t'2': 'hsl(var(--chart-2))',\n  \t\t\t\t'3': 'hsl(var(--chart-3))',\n  \t\t\t\t'4': 'hsl(var(--chart-4))',\n  \t\t\t\t'5': 'hsl(var(--chart-5))'\n  \t\t\t}\n  \t\t},\n  \t\tborderRadius: {\n  \t\t\tlg: 'var(--radius)',\n  \t\t\tmd: 'calc(var(--radius) - 2px)',\n  \t\t\tsm: 'calc(var(--radius) - 4px)'\n  \t\t}\n  \t}\n  },\n  plugins: [require(\"tailwindcss-animate\")],\n};\nexport default config;\n"
  },
  {
    "path": "financial-data-analyst/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "financial-data-analyst/types/chart.ts",
    "content": "// types/chart.ts\nexport interface ChartConfig {\n  [key: string]: {\n    label: string;\n    stacked?: boolean;\n    color?: string;\n  };\n}\n\nexport interface ChartData {\n  chartType: \"bar\" | \"multiBar\" | \"line\" | \"pie\" | \"area\" | \"stackedArea\";\n  config: {\n    title: string;\n    description: string;\n    trend?: {\n      percentage: number;\n      direction: \"up\" | \"down\";\n    };\n    footer?: string;\n    totalLabel?: string;\n    xAxisKey?: string;\n  };\n  data: Array<Record<string, any>>;\n  chartConfig: ChartConfig;\n}\n"
  },
  {
    "path": "financial-data-analyst/utils/fileHandling.ts",
    "content": "// utils/fileHandling.ts\nexport const readFileAsText = (file: File): Promise<string> => {\n  return new Promise((resolve, reject) => {\n    const reader = new FileReader();\n    reader.onload = () => {\n      try {\n        const result = reader.result;\n        if (typeof result === \"string\" && result.length > 0) {\n          resolve(result);\n        } else {\n          reject(new Error(\"Empty or invalid text file\"));\n        }\n      } catch (e) {\n        reject(e);\n      }\n    };\n    reader.onerror = reject;\n    reader.readAsText(file);\n  });\n};\n\nexport const readFileAsBase64 = (file: File): Promise<string> => {\n  return new Promise((resolve, reject) => {\n    const reader = new FileReader();\n    reader.onload = () => {\n      try {\n        const base64 = (reader.result as string).split(\",\")[1];\n        resolve(base64);\n      } catch (e) {\n        reject(e);\n      }\n    };\n    reader.onerror = reject;\n    reader.readAsDataURL(file);\n  });\n};\n\nexport const readFileAsPDFText = async (file: File): Promise<string> => {\n  return new Promise((resolve, reject) => {\n    const script = document.createElement(\"script\");\n    script.src = \"//cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js\";\n\n    script.onload = async () => {\n      try {\n        // @ts-ignore - PDF.js adds this to window\n        const pdfjsLib = window[\"pdfjs-dist/build/pdf\"];\n        pdfjsLib.GlobalWorkerOptions.workerSrc =\n          \"//cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js\";\n\n        const arrayBuffer = await file.arrayBuffer();\n        const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;\n\n        let fullText = \"\";\n\n        for (let i = 1; i <= pdf.numPages; i++) {\n          const page = await pdf.getPage(i);\n          const textContent = await page.getTextContent();\n\n          let lastY: number | null = null;\n          let text = \"\";\n\n          for (const item of textContent.items) {\n            if (lastY !== null && Math.abs(lastY - item.transform[5]) > 5) {\n              text += \"\\n\";\n            } else if (lastY !== null && text.length > 0) {\n              text += \" \";\n            }\n\n            text += item.str;\n            lastY = item.transform[5];\n          }\n\n          fullText += text + \"\\n\\n\";\n        }\n\n        document.body.removeChild(script);\n        resolve(fullText.trim());\n      } catch (error) {\n        document.body.removeChild(script);\n        reject(error);\n      }\n    };\n\n    script.onerror = () => {\n      document.body.removeChild(script);\n      reject(new Error(\"Failed to load PDF.js library\"));\n    };\n\n    document.body.appendChild(script);\n  });\n};\n\n// Update the type definition\nexport interface FileUpload {\n  base64: string;\n  fileName: string;\n  mediaType: string;\n  isText?: boolean;\n  fileSize?: number; // Optional: Add file size information\n}\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[tool.pyright]\nvenvPath = \"computer-use-demo\"\nvenv = \".venv\"\nuseLibraryCodeForTypes = false"
  }
]