[
  {
    "path": ".gitattributes",
    "content": "*.ipynb linguist-detectable=false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\r\nname: Bug report\r\nabout: Create a report to help us improve\r\ntitle: ''\r\nlabels: [\"bug\"]\r\nassignees: ''\r\n\r\n---\r\n\r\n**Describe the bug**\r\nA clear and concise description of what the bug is.\r\n\r\n**To Reproduce**\r\nSteps to reproduce the behavior:\r\n1. Go to '...'\r\n2. Click on '....'\r\n3. Scroll down to '....'\r\n4. See error\r\n\r\n**Expected behavior**\r\nA clear and concise description of what you expected to happen.\r\n\r\n**Error logs/Screenshots**\r\nIf applicable, add logs/screenshots to give more information about the issue.\r\n\r\n**Desktop (please complete the following information where):**\r\n - OS: [e.g. Ubuntu]\r\n - Version: [e.g. 20.04]\r\n - Python: [3.9]\r\n - Vanna: [2.8.0]\r\n\r\n**Additional context**\r\nAdd any other context about the problem here.\r\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\r\nname: Feature request\r\nabout: Suggest an idea for this project\r\ntitle: ''\r\nlabels: [\"enhancements\"]\r\nassignees: ''\r\n\r\n---\r\n\r\n**Is your feature request related to a problem? Please describe.**\r\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\r\n\r\n**Describe the solution you'd like**\r\nA clear and concise description of what you want to happen.\r\n\r\n**Describe alternatives you've considered**\r\nA clear and concise description of any alternative solutions or features you've considered.\r\n\r\n**Additional context**\r\nAdd any other context or screenshots about the feature request here.\r\n"
  },
  {
    "path": ".github/workflows/python-publish.yaml",
    "content": "# This workflow will upload a Python Package using Twine when a release is created\n# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries\n\n# This workflow uses actions that are not certified by GitHub.\n# They are provided by a third-party and are governed by\n# separate terms of service, privacy policy, and support\n# documentation.\n\nname: Upload Python Package\n\non:\n  release:\n    types: [published]\n\npermissions:\n  contents: read\n\njobs:\n  deploy:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v3\n    - name: Set up Python\n      uses: actions/setup-python@v3\n      with:\n        python-version: '3.x'\n    - name: Install dependencies\n      run: |\n        python -m pip install --upgrade pip\n        pip install build\n    - name: Build package\n      run: python -m build\n    - name: Publish package\n      uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29\n      with:\n        user: __token__\n        password: ${{ secrets.PYPI_API_TOKEN }}"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: Basic Integration Tests\n\non:\n  push:\n    branches:\n      - main\n\npermissions:\n  contents: read\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v4\n    - name: Set up Python 3.11\n      uses: actions/setup-python@v5\n      with:\n        python-version: \"3.11\"\n    - name: Install pip\n      run: |\n        python -m pip install --upgrade pip\n        pip install tox\n    - name: Run tests\n      env:\n        PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION: python\n        VANNA_API_KEY: ${{ secrets.VANNA_API_KEY }}\n        OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\n        MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}\n        ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\n        GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}\n        SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}\n        SNOWFLAKE_USERNAME: ${{ secrets.SNOWFLAKE_USERNAME }}\n        SNOWFLAKE_PASSWORD: ${{ secrets.SNOWFLAKE_PASSWORD }}\n      run: tox\n"
  },
  {
    "path": ".gitignore",
    "content": "build\n**.egg-info\nvenn\n.DS_Store\ntests/__pycache__\n__pycache__/\n.idea\n.coverage\ndocs/*.html\n.ipynb_checkpoints/\n.tox/\nnotebooks/chroma.sqlite3\ndist\n.env\n*.sqlite\nhtmlcov\nchroma.sqlite3\n*.bin\n.coverage.*\nmilvus.db\n.milvus.db.lock\n\n# Frontend builds and dependencies\nfrontends/**/node_modules/\nfrontends/**/static/\nfrontends/**/.storybook-static/\nfrontends/**/package-lock.json\nfrontends/**/.mypy_cache/\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "exclude: 'docs|node_modules|migrations|.git|.tox|assets.py'\ndefault_stages: [ commit ]\nfail_fast: true\n\nrepos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v3.2.0\n    hooks:\n      - id: trailing-whitespace\n      - id: end-of-file-fixer\n      - id: check-merge-conflict\n      - id: debug-statements\n      - id: mixed-line-ending\n\n  - repo: https://github.com/pycqa/isort\n    rev: 5.12.0\n    hooks:\n      - id: isort\n        args: [ \"--profile\", \"black\", \"--filter-files\" ]\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Vanna\n\nThank you for your interest in contributing to Vanna! This guide will help you get started with contributing to the Vanna 2.0+ codebase.\n\n## Table of Contents\n\n- [Getting Started](#getting-started)\n- [Development Setup](#development-setup)\n- [Code Standards](#code-standards)\n- [Testing](#testing)\n- [Pull Request Process](#pull-request-process)\n- [Architecture Overview](#architecture-overview)\n- [Adding New Features](#adding-new-features)\n\n---\n\n## Getting Started\n\n### Prerequisites\n\n- Python 3.11 or higher\n- Git\n- A GitHub account\n\n### Fork and Clone\n\n1. Fork the repository on GitHub\n2. Clone your fork locally:\n   ```bash\n   git clone https://github.com/YOUR_USERNAME/vanna.git\n   cd vanna\n   ```\n\n3. Add the upstream repository:\n   ```bash\n   git remote add upstream https://github.com/vanna-ai/vanna.git\n   ```\n\n---\n\n## Development Setup\n\n### 1. Create a Virtual Environment\n\n```bash\npython3 -m venv venv\nsource venv/bin/activate  # On Windows: venv\\Scripts\\activate\n```\n\n### 2. Install Dependencies\n\n```bash\n# Install the package in editable mode with all extras\npip install -e \".[all]\"\n\n# Install development tools\npip install tox ruff mypy pytest pytest-asyncio\n```\n\n### 3. Verify Installation\n\n```bash\n# Run unit tests\ntox -e py311-unit\n\n# Run type checking\ntox -e mypy\n\n# Run format checking\ntox -e ruff\n```\n\n---\n\n## Code Standards\n\n### Formatting\n\nWe use [ruff](https://github.com/astral-sh/ruff) for code formatting and linting.\n\n```bash\n# Check formatting\nruff format --check src/vanna/ tests/\n\n# Apply formatting\nruff format src/vanna/ tests/\n\n# Run linting\nruff check src/vanna/ tests/\n```\n\n### Type Checking\n\nWe use mypy with strict mode for type checking:\n\n```bash\ntox -e mypy\n```\n\nAll new code should include type hints.\n\n### Code Style Guidelines\n\n- Follow PEP 8 style guidelines\n- Use descriptive variable and function names\n- Add docstrings to all public functions and classes\n- Keep functions focused and single-purpose\n- Avoid circular imports by using `TYPE_CHECKING`\n\n**Example:**\n\n```python\n\"\"\"Module docstring explaining the purpose.\"\"\"\n\nfrom typing import TYPE_CHECKING, Optional\n\nif TYPE_CHECKING:\n    from vanna.core.user import User\n\nclass MyClass:\n    \"\"\"Class docstring explaining what this class does.\"\"\"\n\n    async def my_method(self, user: \"User\", count: int = 10) -> Optional[str]:\n        \"\"\"Method docstring explaining parameters and return value.\n\n        Args:\n            user: The user making the request\n            count: Maximum number of items to return\n\n        Returns:\n            Result string if found, None otherwise\n        \"\"\"\n        pass\n```\n\n---\n\n## Testing\n\n### Test Organization\n\nTests are organized in the `tests/` directory:\n\n- `test_tool_permissions.py` - Tool access control tests\n- `test_llm_context_enhancer.py` - LLM enhancer tests\n- `test_legacy_adapter.py` - Legacy compatibility tests\n- `test_agent_memory.py` - Agent memory tests\n- `test_database_sanity.py` - Database integration tests\n- `test_agents.py` - End-to-end agent tests\n\n### Running Tests\n\n```bash\n# Run all unit tests (no external dependencies)\ntox -e py311-unit\n\n# Run specific test file\npytest tests/test_tool_permissions.py -v\n\n# Run tests with a specific marker\npytest tests/ -v -m anthropic\n\n# Run legacy adapter tests\ntox -e py311-legacy\n```\n\n### Writing Tests\n\n1. **Unit tests** should not require external dependencies (databases, APIs, etc.)\n2. Use **pytest markers** for tests that require external services:\n   ```python\n   @pytest.mark.anthropic\n   @pytest.mark.asyncio\n   async def test_with_anthropic():\n       # Test code here\n       pass\n   ```\n\n3. **Mock external dependencies** in unit tests:\n   ```python\n   class MockLlmService(LlmService):\n       async def send_request(self, request):\n           # Mock implementation\n           pass\n   ```\n\n4. **Test both success and failure cases**\n5. **Use descriptive test names** that explain what is being tested\n\n### Test Coverage\n\nWhen adding new features, ensure:\n- Core functionality is covered by unit tests\n- Integration points are tested\n- Error handling is validated\n- Edge cases are considered\n\n---\n\n## Pull Request Process\n\n### 1. Create a Feature Branch\n\n```bash\ngit checkout -b feature/my-new-feature\n# or\ngit checkout -b fix/bug-description\n```\n\n### 2. Make Your Changes\n\n- Write your code following the code standards\n- Add tests for your changes\n- Update documentation as needed\n\n### 3. Run All Checks\n\n```bash\n# Format code\nruff format src/vanna/ tests/\n\n# Run linting\nruff check src/vanna/ tests/\n\n# Run type checking\ntox -e mypy\n\n# Run tests\ntox -e py311-unit\n```\n\n### 4. Commit Your Changes\n\nUse clear, descriptive commit messages:\n\n```bash\ngit add .\ngit commit -m \"feat: add new LLM context enhancer for RAG\n\n- Implements TextMemoryEnhancer class\n- Adds tests for memory retrieval\n- Updates documentation\"\n```\n\n**Commit message format:**\n- `feat:` - New feature\n- `fix:` - Bug fix\n- `docs:` - Documentation changes\n- `test:` - Adding or updating tests\n- `refactor:` - Code refactoring\n- `chore:` - Maintenance tasks\n\n### 5. Push and Create PR\n\n```bash\ngit push origin feature/my-new-feature\n```\n\nThen create a pull request on GitHub with:\n- Clear title describing the change\n- Description of what was changed and why\n- Link to any related issues\n- Screenshots or examples if applicable\n\n### 6. Code Review\n\n- Address review feedback promptly\n- Keep discussions focused and professional\n- Be open to suggestions and alternative approaches\n\n---\n\n## Architecture Overview\n\n### Core Components\n\nVanna 2.0+ is built around several key abstractions:\n\n#### 1. **Agent** (`vanna.core.agent`)\nThe main orchestrator that coordinates tools, memory, and LLM interactions.\n\n#### 2. **Tools** (`vanna.tools`, `vanna.core.tool`)\nModular capabilities that the agent can use. Each tool:\n- Has a schema defining its inputs\n- Implements an `execute()` method\n- Declares access control via `access_groups`\n\n#### 3. **Tool Registry** (`vanna.core.registry`)\nManages tool registration and access control.\n\n#### 4. **Agent Memory** (`vanna.capabilities.agent_memory`)\nStores and retrieves tool usage patterns and documentation.\n\n#### 5. **LLM Services** (`vanna.core.llm`)\nAbstract interface for different LLM providers (Anthropic, OpenAI, etc.).\n\n#### 6. **SQL Runners** (`vanna.capabilities.sql_runner`)\nAbstract interface for executing SQL against different databases.\n\n#### 7. **Components** (`vanna.components`)\nRich UI components for rendering results (tables, charts, status cards, etc.).\n\n### Data Flow\n\n```\nUser Request → Agent → LLM Service → Tool Selection → Tool Execution → Response Components\n                ↓                                           ↓\n          Agent Memory                              SQL Runner / Other Capabilities\n```\n\n---\n\n## Adding New Features\n\n### Adding a New Tool\n\n1. **Create the tool class** in `src/vanna/tools/`:\n\n```python\nfrom vanna.core.tool import Tool, ToolContext, ToolResult\nfrom pydantic import BaseModel, Field\n\nclass MyToolArgs(BaseModel):\n    \"\"\"Arguments for my tool.\"\"\"\n    query: str = Field(description=\"The query to process\")\n\nclass MyTool(Tool[MyToolArgs]):\n    \"\"\"Tool that does something useful.\"\"\"\n\n    @property\n    def name(self) -> str:\n        return \"my_tool\"\n\n    @property\n    def description(self) -> str:\n        return \"Does something useful with a query\"\n\n    def get_args_schema(self) -> type[MyToolArgs]:\n        return MyToolArgs\n\n    async def execute(\n        self,\n        context: ToolContext,\n        args: MyToolArgs\n    ) -> ToolResult:\n        # Implement your tool logic\n        result = f\"Processed: {args.query}\"\n\n        return ToolResult(\n            success=True,\n            result_for_llm=result,\n            ui_component=None\n        )\n```\n\n2. **Add tests** in `tests/test_my_tool.py`\n\n3. **Register the tool** in examples or documentation\n\n### Adding a New Database Integration\n\n1. **Implement SqlRunner** in `src/vanna/integrations/mydb/`:\n\n```python\nfrom vanna.capabilities.sql_runner import SqlRunner, RunSqlToolArgs\nfrom vanna.core.tool import ToolContext\nimport pandas as pd\n\nclass MyDbRunner(SqlRunner):\n    \"\"\"SQL runner for MyDB database.\"\"\"\n\n    def __init__(self, connection_string: str):\n        self.connection_string = connection_string\n        # Initialize your DB connection\n\n    async def run_sql(\n        self,\n        args: RunSqlToolArgs,\n        context: ToolContext\n    ) -> pd.DataFrame:\n        # Execute SQL and return DataFrame\n        pass\n```\n\n2. **Add sanity tests** in `tests/test_database_sanity.py`\n\n3. **Add tox target** in `tox.ini`\n\n4. **Update documentation**\n\n### Adding a New LLM Integration\n\n1. **Implement LlmService** in `src/vanna/integrations/myllm/`:\n\n```python\nfrom vanna.core.llm.base import LlmService\nfrom vanna.core.llm.models import LlmRequest, LlmResponse, LlmStreamChunk\nfrom typing import AsyncGenerator\n\nclass MyLlmService(LlmService):\n    \"\"\"LLM service for MyLLM provider.\"\"\"\n\n    def __init__(self, api_key: str, model: str = \"default\"):\n        self.api_key = api_key\n        self.model = model\n\n    async def send_request(self, request: LlmRequest) -> LlmResponse:\n        # Implement API call\n        pass\n\n    async def stream_request(\n        self,\n        request: LlmRequest\n    ) -> AsyncGenerator[LlmStreamChunk, None]:\n        # Implement streaming API call\n        yield LlmStreamChunk(...)\n\n    async def validate_tools(self, tools) -> list[str]:\n        # Validate tool schemas\n        return []\n```\n\n2. **Add tests** with the `@pytest.mark.myllm` marker\n\n3. **Add tox target** for integration tests\n\n### Adding a New Agent Memory Backend\n\n1. **Implement AgentMemory** in `src/vanna/integrations/mystore/`:\n\n```python\nfrom vanna.capabilities.agent_memory import (\n    AgentMemory,\n    ToolMemory,\n    ToolMemorySearchResult,\n    TextMemory,\n    TextMemorySearchResult\n)\nfrom vanna.core.tool import ToolContext\n\nclass MyStoreMemory(AgentMemory):\n    \"\"\"Agent memory using MyStore vector database.\"\"\"\n\n    async def save_tool_usage(self, question, tool_name, args, context, success=True, metadata=None):\n        # Implement storage\n        pass\n\n    async def search_similar_usage(self, question, context, *, limit=10, similarity_threshold=0.7, tool_name_filter=None):\n        # Implement search\n        pass\n\n    # Implement other AgentMemory methods...\n```\n\n2. **Add tests** in `tests/test_agent_memory.py`\n\n3. **Add to extras** in `pyproject.toml`\n\n---\n\n## Legacy Compatibility\n\nIf you're working on legacy VannaBase compatibility:\n\n- The `LegacyVannaAdapter` bridges legacy code with Vanna 2.0+\n- Add tests to `tests/test_legacy_adapter.py`\n- See `src/vanna/legacy/adapter.py` for examples\n\n---\n\n## Getting Help\n\n- **Documentation**: https://vanna.ai/docs/\n- **GitHub Issues**: https://github.com/vanna-ai/vanna/issues\n- **Discussions**: https://github.com/vanna-ai/vanna/discussions\n\n---\n\n## License\n\nBy contributing to Vanna, you agree that your contributions will be licensed under the MIT License.\n\n---\n\nThank you for contributing to Vanna! 🎉\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Vanna.AI\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.\n"
  },
  {
    "path": "MIGRATION_GUIDE.md",
    "content": "# Migration Guide: Vanna 0.x to Vanna 2.0+\n\nThis guide will help you migrate from Vanna 0.x (legacy) to Vanna 2.0+, the new user-aware agent framework.\n\n## Table of Contents\n- [Overview of Changes](#overview-of-changes)\n- [Quick Migration Path](#quick-migration-path)\n- [Migration Strategies](#migration-strategies)\n  - [Strategy 1: Using the Legacy Adapter (Recommended for Quick Migration)](#strategy-1-using-the-legacy-adapter-recommended-for-quick-migration)\n  - [Strategy 2: Full Migration to New Architecture](#strategy-2-full-migration-to-new-architecture)\n- [Key Architectural Differences](#key-architectural-differences)\n- [API Mapping](#api-mapping)\n- [Common Migration Scenarios](#common-migration-scenarios)\n- [Breaking Changes](#breaking-changes)\n- [FAQ](#faq)\n\n---\n\n## Overview of Changes\n\nVanna 2.0+ represents a fundamental architectural shift from a simple LLM wrapper to a full-fledged **user-aware agent framework**. Here are the major changes:\n\n### What's New in 2.0+\n- ✅ **User awareness** - Identity and permissions flow through every layer\n- ✅ **Web component** - Pre-built UI with streaming responses\n- ✅ **Tool registry** - Modular, extensible tool system\n- ✅ **Rich UI components** - Tables, charts, status cards (not just text)\n- ✅ **Streaming by default** - Progressive responses via SSE\n- ✅ **Enterprise features** - Audit logs, rate limiting, observability\n- ✅ **FastAPI/Flask servers** - Production-ready backends included\n\n### What Changed from 0.x\n- ❌ Direct method calls (`vn.ask()`) → Agent-based workflow\n- ❌ Monolithic `VannaBase` class → Modular tool system\n- ❌ No user context → User-aware at every layer\n- ❌ Simple text responses → Rich streaming UI components\n\n---\n\n## Quick Migration Path\n\n**Can't migrate immediately?** Use the Legacy Adapter to get started quickly:\n\n```python\n# Assume you already have a working vn object from your Vanna 0.x code:\n# vn = MyVanna(config={\"model\": \"gpt-4\"})\n# vn.connect_to_postgres(...)\n# vn.train(ddl=\"...\")\n\n# NEW: Just add these imports and wrap your existing vn object\nfrom vanna import Agent, AgentConfig\nfrom vanna.servers.fastapi import VannaFastAPIServer\nfrom vanna.core.user import UserResolver, User, RequestContext\nfrom vanna.legacy.adapter import LegacyVannaAdapter\nfrom vanna.integrations.anthropic import AnthropicLlmService\n\n# Define simple user resolver\nclass SimpleUserResolver(UserResolver):\n    async def resolve_user(self, request_context: RequestContext) -> User:\n        user_email = request_context.get_cookie('vanna_email')\n        return User(id=user_email, email=user_email, group_memberships=['user'])\n\n# Wrap your existing vn with the adapter\ntools = LegacyVannaAdapter(vn)\n\n# Create agent with new LLM service\nllm = AnthropicLlmService(model=\"claude-haiku-4-5\")\nagent = Agent(llm_service=llm, tool_registry=tools, user_resolver=SimpleUserResolver())\n\n# Run server\nserver = VannaFastAPIServer(agent)\nserver.run(host='0.0.0.0', port=8000)\n\n# Now it works with the new Agent framework!\n# (See Strategy 1 below for complete example)\n```\n\n---\n\n## Migration Strategies\n\n### Strategy 1: Using the Legacy Adapter (Recommended for Quick Migration)\n\n**Best for:** Teams that want to adopt Vanna 2.0+ gradually while maintaining existing code.\n\n#### Step 1: Install Vanna 2.0+\n\n```bash\npip install 'vanna[flask,anthropic]'\n```\n\n#### Step 2: Wrap Your Existing VannaBase Instance\n\n```python\nfrom vanna import Agent, AgentConfig\nfrom vanna.servers.fastapi import VannaFastAPIServer\nfrom vanna.core.user import UserResolver, User, RequestContext\nfrom vanna.legacy.adapter import LegacyVannaAdapter\nfrom vanna.integrations.anthropic import AnthropicLlmService\n\n# Assume you already have a working vn object from your existing code:\n# vn = MyVanna(config={'model': 'gpt-4', 'api_key': 'your-key'})\n# vn.connect_to_postgres(...)\n# vn.train(ddl=\"...\")\n# etc.\n\n# NEW: Define user resolution (required in 2.0+)\nclass SimpleUserResolver(UserResolver):\n    async def resolve_user(self, request_context: RequestContext) -> User:\n        user_email = request_context.get_cookie('vanna_email')\n        if not user_email:\n            raise ValueError(\"Missing 'vanna_email' cookie\")\n\n        # Admin users get 'admin' group membership\n        if user_email == \"admin@example.com\":\n            return User(id=\"admin_user\", email=user_email, group_memberships=['admin'])\n\n        # Regular users get 'user' group membership\n        return User(id=user_email, email=user_email, group_memberships=['user'])\n\n# NEW: Wrap with legacy adapter\n# This automatically registers run_sql and memory tools from your VannaBase instance\ntools = LegacyVannaAdapter(vn)\n\n# NEW: Set up LLM for the new Agent framework\nllm = AnthropicLlmService(\n    model=\"claude-haiku-4-5\",\n    api_key=\"YOUR_ANTHROPIC_API_KEY\"\n)\n\n# NEW: Create agent with legacy adapter as tool registry\nagent = Agent(\n    llm_service=llm,\n    tool_registry=tools,  # LegacyVannaAdapter is a ToolRegistry\n    user_resolver=SimpleUserResolver(),\n    config=AgentConfig()\n)\n\n# NEW: Create and run server\nserver = VannaFastAPIServer(agent)\n\nif __name__ == \"__main__\":\n    # Run with: python your_script.py\n    # Or: uvicorn your_module:server --host 0.0.0.0 --port 8000\n    server.run(host='0.0.0.0', port=8000)\n```\n\n**What the LegacyVannaAdapter does:**\n- Automatically wraps `vn.run_sql()` as the `run_sql` tool (available to 'user' and 'admin' groups)\n- Exposes training data from `vn.get_training_data()` as searchable memory (via `search_saved_correct_tool_uses` tool)\n- Optionally allows saving new training data (via `save_question_tool_args` tool - admin only)\n- Maintains your existing database connection and training data\n\n**Pros:**\n- ✅ Minimal code changes\n- ✅ Preserve existing training data\n- ✅ Gradual migration path\n- ✅ Get new features (web UI, streaming) immediately\n\n**Cons:**\n- ⚠️ Limited user awareness (all requests use same VannaBase instance)\n- ⚠️ Can't leverage row-level security\n- ⚠️ Missing some advanced features\n\n---\n\n### Strategy 2: Full Migration to New Architecture\n\n**Best for:** New projects or teams ready for a complete rewrite.\n\n#### Before (Vanna 0.x)\n\n```python\nfrom vanna import VannaBase\nfrom vanna.openai_chat import OpenAI_Chat\nfrom vanna.chromadb import ChromaDB_VectorStore\n\nclass MyVanna(ChromaDB_VectorStore, OpenAI_Chat):\n    def __init__(self, config=None):\n        ChromaDB_VectorStore.__init__(self, config=config)\n        OpenAI_Chat.__init__(self, config=config)\n\nvn = MyVanna(config={'model': 'gpt-4', 'api_key': 'your-key'})\nvn.connect_to_postgres(...)\n\n# Train\nvn.train(ddl=\"CREATE TABLE customers ...\")\nvn.train(question=\"Top customers?\", sql=\"SELECT ...\")\n\n# Ask\nsql = vn.generate_sql(\"Who are the top customers?\")\ndf = vn.run_sql(sql)\nprint(df)\n```\n\n#### After (Vanna 2.0+)\n\n```python\nfrom vanna import Agent, AgentConfig\nfrom vanna.servers.fastapi import VannaFastAPIServer\nfrom vanna.core.registry import ToolRegistry\nfrom vanna.core.user import UserResolver, User, RequestContext\nfrom vanna.integrations.anthropic import AnthropicLlmService\nfrom vanna.tools import RunSqlTool\nfrom vanna.integrations.postgres import PostgresRunner\n\n# 1. Define user resolution\nclass MyUserResolver(UserResolver):\n    async def resolve_user(self, request_context: RequestContext) -> User:\n        # Extract from your auth system (JWT, cookies, etc.)\n        token = request_context.get_header('Authorization')\n        user_data = await self.validate_token(token)\n\n        return User(\n            id=user_data['id'],\n            email=user_data['email'],\n            permissions=user_data['permissions'],\n            metadata={'role': user_data['role']}\n        )\n\n# 2. Set up tools\ntools = ToolRegistry()\npostgres_runner = PostgresRunner(\n    host=\"localhost\",\n    dbname=\"mydb\",\n    user=\"user\",\n    password=\"password\",\n    port=5432\n)\ntools.register_local_tool(\n    RunSqlTool(sql_runner=postgres_runner),\n    access_groups=['user', 'admin']\n)\n\n# 3. Create agent\nllm = AnthropicLlmService(model=\"claude-sonnet-4-5\")\nagent = Agent(\n    llm_service=llm,\n    tool_registry=tools,\n    user_resolver=MyUserResolver(),\n    config=AgentConfig(stream_responses=True)\n)\n\n# 4. Create server\nserver = VannaFastAPIServer(agent)\napp = server.create_app()\n\n# Run with: uvicorn main:app --host 0.0.0.0 --port 8000\n# Visit http://localhost:8000 for web UI\n```\n\n**Pros:**\n- ✅ Full access to new features\n- ✅ True user awareness\n- ✅ Better security and permissions\n- ✅ Production-ready architecture\n\n**Cons:**\n- ⚠️ Requires rewriting code\n- ⚠️ Need to migrate training data approach\n- ⚠️ Steeper learning curve\n\n---\n\n## Key Architectural Differences\n\n| Feature | Vanna 0.x | Vanna 2.0+ |\n|---------|-----------|------------|\n| **User Context** | None | `User` object with permissions flows through entire system |\n| **Interaction Model** | Direct method calls (`vn.ask()`) | Agent-based with streaming components |\n| **Tools** | Monolithic methods | Modular `Tool` classes with schemas |\n| **Responses** | Plain text/DataFrames | Rich UI components (tables, charts, code) |\n| **Training** | `vn.train()` with vector DB | System prompts, context enrichers, RAG tools |\n| **Database Connection** | `vn.connect_to_postgres()` | `SqlRunner` implementations as dependencies |\n| **Web UI** | None (custom implementation) | Built-in web component + backend |\n| **Streaming** | None | Server-Sent Events by default |\n| **Permissions** | None | Group-based access control on tools |\n| **Audit Logs** | None | Built-in audit logging system |\n\n---\n\n## Summary\n\n| If you want to... | Use this strategy |\n|-------------------|-------------------|\n| Migrate quickly with minimal changes | **Strategy 1: Legacy Adapter** |\n| Get full access to new features | **Strategy 2: Full Migration** |\n| Support both legacy and new code | **Strategy 1** initially, then gradual migration |\n| Start a new project | **Strategy 2: Full Migration** |\n\n**Recommended Path:**\n1. Start with Legacy Adapter for quick migration\n2. Gradually rewrite critical paths to native 2.0+ architecture\n3. Eventually remove Legacy Adapter once fully migrated\n\nGood luck with your migration! 🚀\n"
  },
  {
    "path": "README.md",
    "content": "# Vanna 2.0: Turn Questions into Data Insights\n\n**Natural language → SQL → Answers.** Now with enterprise security and user-aware permissions.\n\n[![Python](https://img.shields.io/badge/python-3.8+-blue.svg)](https://python.org)\n[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)\n[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)\n\nhttps://github.com/user-attachments/assets/476cd421-d0b0-46af-8b29-0f40c73d6d83\n\n\n![Vanna2 Demo](img/architecture.png)\n\n---\n\n## What's New in 2.0\n\n🔐 **User-Aware at Every Layer** — Queries automatically filtered per user permissions\n\n🎨 **Modern Web Interface** — Beautiful pre-built `<vanna-chat>` component\n\n⚡ **Streaming Responses** — Real-time tables, charts, and progress updates\n\n🔒 **Enterprise Security** — Row-level security, audit logs, rate limiting\n\n🔄 **Production-Ready** — FastAPI integration, observability, lifecycle hooks\n\n> **Upgrading from 0.x?** See the [Migration Guide](MIGRATION_GUIDE.md) | [What changed?](#migration-notes)\n\n---\n\n## Get Started\n\n### Try it with Sample Data\n\n[Quickstart](https://vanna.ai/docs/quick-start)\n\n### Configure\n\n[Configure](https://vanna.ai/docs/configure)\n\n### Web Component\n\n```html\n<!-- Drop into any existing webpage -->\n<script src=\"https://img.vanna.ai/vanna-components.js\"></script>\n<vanna-chat\n  sse-endpoint=\"https://your-api.com/chat\"\n  theme=\"dark\">\n</vanna-chat>\n```\n\nUses your existing cookies/JWTs. Works with React, Vue, or plain HTML.\n\n---\n\n## What You Get\n\nAsk a question in natural language and get back:\n\n**1. Streaming Progress Updates**\n\n**2. SQL Code Block (By default only shown to \"admin\" users)**\n\n**3. Interactive Data Table**\n\n**4. Charts** (Plotly visualizations)\n\n**5. Natural Language Summary**\n\nAll streamed in real-time to your web component.\n\n---\n\n## Why Vanna 2.0?\n\n### ✅ Get Started Instantly\n* Production chat interface\n* Custom agent with your database\n* Embed in any webpage\n\n### ✅ Enterprise-Ready Security\n**User-aware at every layer** — Identity flows through system prompts, tool execution, and SQL filtering\n**Row-level security** — Queries automatically filtered per user permissions\n**Audit logs** — Every query tracked per user for compliance\n**Rate limiting** — Per-user quotas via lifecycle hooks\n\n### ✅ Beautiful Web UI Included\n**Pre-built `<vanna-chat>` component** — No need to build your own chat interface\n**Streaming tables & charts** — Rich components, not just text\n**Responsive & customizable** — Works on mobile, desktop, light/dark themes\n**Framework-agnostic** — React, Vue, plain HTML\n\n### ✅ Works With Your Stack\n**Any LLM:** OpenAI, Anthropic, Ollama, Azure, Google Gemini, AWS Bedrock, Mistral, Others\n**Any Database:** PostgreSQL, MySQL, Snowflake, BigQuery, Redshift, SQLite, Oracle, SQL Server, DuckDB, ClickHouse, Others\n**Your Auth System:** Bring your own — cookies, JWTs, OAuth tokens\n**Your Framework:** FastAPI, Flask\n\n### ✅ Extensible But Opinionated\n**Custom tools** — Extend the `Tool` base class\n**Lifecycle hooks** — Quota checking, logging, content filtering\n**LLM middlewares** — Caching, prompt engineering\n**Observability** — Built-in tracing and metrics\n\n---\n\n## Architecture\n\n![Vanna2 Diagram](img/vanna2.svg)\n\n---\n\n## How It Works\n\n```mermaid\nsequenceDiagram\n    participant U as 👤 User\n    participant W as 🌐 <vanna-chat>\n    participant S as 🐍 Your Server\n    participant A as 🤖 Agent\n    participant T as 🧰 Tools\n\n    U->>W: \"Show Q4 sales\"\n    W->>S: POST /api/vanna/v2/chat_sse (with auth)\n    S->>A: User(id=alice, groups=[read_sales])\n    A->>T: Execute SQL tool (user-aware)\n    T->>T: Apply row-level security\n    T->>A: Filtered results\n    A->>W: Stream: Table → Chart → Summary\n    W->>U: Display beautiful UI\n```\n\n**Key Concepts:**\n\n1. **User Resolver** — You define how to extract user identity from requests (cookies, JWTs, etc.)\n2. **User-Aware Tools** — Tools automatically check permissions based on user's group memberships\n3. **Streaming Components** — Backend streams structured UI components (tables, charts) to frontend\n4. **Built-in Web UI** — Pre-built `<vanna-chat>` component renders everything beautifully\n\n---\n\n## Production Setup with Your Auth\n\nHere's a complete example integrating Vanna with your existing FastAPI app and authentication:\n\n```python\nfrom fastapi import FastAPI\nfrom vanna import Agent\nfrom vanna.servers.fastapi.routes import register_chat_routes\nfrom vanna.servers.base import ChatHandler\nfrom vanna.core.user import UserResolver, User, RequestContext\nfrom vanna.integrations.anthropic import AnthropicLlmService\nfrom vanna.tools import RunSqlTool\nfrom vanna.integrations.sqlite import SqliteRunner\nfrom vanna.core.registry import ToolRegistry\n\n# Your existing FastAPI app\napp = FastAPI()\n\n# 1. Define your user resolver (using YOUR auth system)\nclass MyUserResolver(UserResolver):\n    async def resolve_user(self, request_context: RequestContext) -> User:\n        # Extract from cookies, JWTs, or session\n        token = request_context.get_header('Authorization')\n        user_data = self.decode_jwt(token)  # Your existing logic\n\n        return User(\n            id=user_data['id'],\n            email=user_data['email'],\n            group_memberships=user_data['groups']  # Used for permissions\n        )\n\n# 2. Set up agent with tools\nllm = AnthropicLlmService(model=\"claude-sonnet-4-5\")\ntools = ToolRegistry()\ntools.register(RunSqlTool(sql_runner=SqliteRunner(\"./data.db\")))\n\nagent = Agent(\n    llm_service=llm,\n    tool_registry=tools,\n    user_resolver=MyUserResolver()\n)\n\n# 3. Add Vanna routes to your app\nchat_handler = ChatHandler(agent)\nregister_chat_routes(app, chat_handler)\n\n# Now you have:\n# - POST /api/vanna/v2/chat_sse (streaming endpoint)\n# - GET / (optional web UI)\n```\n\n**Then in your frontend:**\n```html\n<vanna-chat sse-endpoint=\"/api/vanna/v2/chat_sse\"></vanna-chat>\n```\n\nSee [Full Documentation](https://vanna.ai/docs) for custom tools, lifecycle hooks, and advanced configuration\n\n---\n\n## Custom Tools\n\nExtend Vanna with custom tools for your specific use case:\n\n```python\nfrom vanna.core.tool import Tool, ToolContext, ToolResult\nfrom pydantic import BaseModel, Field\nfrom typing import Type\n\nclass EmailArgs(BaseModel):\n    recipient: str = Field(description=\"Email recipient\")\n    subject: str = Field(description=\"Email subject\")\n\nclass EmailTool(Tool[EmailArgs]):\n    @property\n    def name(self) -> str:\n        return \"send_email\"\n\n    @property\n    def access_groups(self) -> list[str]:\n        return [\"send_email\"]  # Permission check\n\n    def get_args_schema(self) -> Type[EmailArgs]:\n        return EmailArgs\n\n    async def execute(self, context: ToolContext, args: EmailArgs) -> ToolResult:\n        user = context.user  # Automatically injected\n\n        # Your business logic\n        await self.email_service.send(\n            from_email=user.email,\n            to=args.recipient,\n            subject=args.subject\n        )\n\n        return ToolResult(success=True, result_for_llm=f\"Email sent to {args.recipient}\")\n\n# Register your tool\ntools.register(EmailTool())\n```\n\n---\n\n## Advanced Features\n\nVanna 2.0 includes powerful enterprise features for production use:\n\n**Lifecycle Hooks** — Add quota checking, custom logging, content filtering at key points in the request lifecycle\n\n**LLM Middlewares** — Implement caching, prompt engineering, or cost tracking around LLM calls\n\n**Conversation Storage** — Persist and retrieve conversation history per user\n\n**Observability** — Built-in tracing and metrics integration\n\n**Context Enrichers** — Add RAG, memory, or documentation to enhance agent responses\n\n**Agent Configuration** — Control streaming, temperature, max iterations, and more\n\n---\n\n## Use Cases\n\n**Vanna is ideal for:**\n- 📊 Data analytics applications with natural language interfaces\n- 🔐 Multi-tenant SaaS needing user-aware permissions\n- 🎨 Teams wanting a pre-built web component + backend\n- 🏢 Enterprise environments with security/audit requirements\n- 📈 Applications needing rich streaming responses (tables, charts, SQL)\n- 🔄 Integrating with existing authentication systems\n\n---\n\n## Community & Support\n\n- 📖 **[Full Documentation](https://vanna.ai/docs)** — Complete guides and API reference\n- 💡 **[GitHub Discussions](https://github.com/vanna-ai/vanna/discussions)** — Feature requests and Q&A\n- 🐛 **[GitHub Issues](https://github.com/vanna-ai/vanna/issues)** — Bug reports\n- 📧 **Enterprise Support** — support@vanna.ai\n\n---\n\n## Migration Notes\n\n**Upgrading from Vanna 0.x?**\n\nVanna 2.0 is a complete rewrite focused on user-aware agents and production deployments. Key changes:\n\n- **New API**: Agent-based instead of `VannaBase` class methods\n- **User-aware**: Every component now knows the user identity\n- **Streaming**: Rich UI components instead of text/dataframes\n- **Web-first**: Built-in `<vanna-chat>` component and server\n\n**Migration path:**\n\n1. **Quick wrap** — Use `LegacyVannaAdapter` to wrap your existing Vanna 0.x instance and get the new web UI immediately\n2. **Gradual migration** — Incrementally move to the new Agent API and tools\n\nSee the complete [Migration Guide](MIGRATION_GUIDE.md) for step-by-step instructions.\n\n---\n\n## License\n\nMIT License — See [LICENSE](LICENSE) for details.\n\n---\n\n**Built with ❤️ by the Vanna team** | [Website](https://vanna.ai) | [Docs](https://vanna.ai/docs) | [Discussions](https://github.com/vanna-ai/vanna/discussions)\n"
  },
  {
    "path": "README_LEGACY.md",
    "content": "\n\n| GitHub | PyPI | Documentation | Gurubase |\n| ------ | ---- | ------------- | -------- |\n| [![GitHub](https://img.shields.io/badge/GitHub-vanna-blue?logo=github)](https://github.com/vanna-ai/vanna) | [![PyPI](https://img.shields.io/pypi/v/vanna?logo=pypi)](https://pypi.org/project/vanna/) | [![Documentation](https://img.shields.io/badge/Documentation-vanna-blue?logo=read-the-docs)](https://vanna.ai/docs/) | [![Gurubase](https://img.shields.io/badge/Gurubase-Ask%20Vanna%20Guru-006BFF)](https://gurubase.io/g/vanna) |\n\n# Vanna\nVanna is an MIT-licensed open-source Python RAG (Retrieval-Augmented Generation) framework for SQL generation and related functionality.\n\nhttps://github.com/vanna-ai/vanna/assets/7146154/1901f47a-515d-4982-af50-f12761a3b2ce\n\n![vanna-quadrants](https://github.com/vanna-ai/vanna/assets/7146154/1c7c88ba-c144-4ecf-a028-cf5ba7344ca2)\n\n## How Vanna works\n\n![Screen Recording 2024-01-24 at 11 21 37 AM](https://github.com/vanna-ai/vanna/assets/7146154/1d2718ad-12a8-4a76-afa2-c61754462f93)\n\n\nVanna works in two easy steps - train a RAG \"model\" on your data, and then ask questions which will return SQL queries that can be set up to automatically run on your database.\n\n1. **Train a RAG \"model\" on your data**.\n2. **Ask questions**.\n\n![](img/vanna-readme-diagram.png)\n\nIf you don't know what RAG is, don't worry -- you don't need to know how this works under the hood to use it. You just need to know that you \"train\" a model, which stores some metadata and then use it to \"ask\" questions.\n\nSee the [base class](https://github.com/vanna-ai/vanna/blob/main/src/vanna/base/base.py) for more details on how this works under the hood.\n\n## User Interfaces\nThese are some of the user interfaces that we've built using Vanna. You can use these as-is or as a starting point for your own custom interface.\n\n- [Jupyter Notebook](https://vanna.ai/docs/postgres-openai-vanna-vannadb/)\n- [vanna-ai/vanna-streamlit](https://github.com/vanna-ai/vanna-streamlit)\n- [vanna-ai/vanna-flask](https://github.com/vanna-ai/vanna-flask)\n- [vanna-ai/vanna-slack](https://github.com/vanna-ai/vanna-slack)\n\n## Supported LLMs\n\n- [OpenAI](https://github.com/vanna-ai/vanna/tree/main/src/vanna/openai)\n- [Anthropic](https://github.com/vanna-ai/vanna/tree/main/src/vanna/anthropic)\n- [Gemini](https://github.com/vanna-ai/vanna/blob/main/src/vanna/google/gemini_chat.py)\n- [HuggingFace](https://github.com/vanna-ai/vanna/blob/main/src/vanna/hf/hf.py)\n- [AWS Bedrock](https://github.com/vanna-ai/vanna/tree/main/src/vanna/bedrock)\n- [Ollama](https://github.com/vanna-ai/vanna/tree/main/src/vanna/ollama)\n- [Qianwen](https://github.com/vanna-ai/vanna/tree/main/src/vanna/qianwen)\n- [Qianfan](https://github.com/vanna-ai/vanna/tree/main/src/vanna/qianfan)\n- [Zhipu](https://github.com/vanna-ai/vanna/tree/main/src/vanna/ZhipuAI)\n\n## Supported VectorStores\n\n- [AzureSearch](https://github.com/vanna-ai/vanna/tree/main/src/vanna/azuresearch)\n- [Opensearch](https://github.com/vanna-ai/vanna/tree/main/src/vanna/opensearch)\n- [PgVector](https://github.com/vanna-ai/vanna/tree/main/src/vanna/pgvector)\n- [PineCone](https://github.com/vanna-ai/vanna/tree/main/src/vanna/pinecone)\n- [ChromaDB](https://github.com/vanna-ai/vanna/tree/main/src/vanna/chromadb)\n- [FAISS](https://github.com/vanna-ai/vanna/tree/main/src/vanna/faiss)\n- [Marqo](https://github.com/vanna-ai/vanna/tree/main/src/vanna/marqo)\n- [Milvus](https://github.com/vanna-ai/vanna/tree/main/src/vanna/milvus)\n- [Qdrant](https://github.com/vanna-ai/vanna/tree/main/src/vanna/qdrant)\n- [Weaviate](https://github.com/vanna-ai/vanna/tree/main/src/vanna/weaviate)\n- [Oracle](https://github.com/vanna-ai/vanna/tree/main/src/vanna/oracle)\n\n## Supported Databases\n\n- [PostgreSQL](https://www.postgresql.org/)\n- [MySQL](https://www.mysql.com/)\n- [PrestoDB](https://prestodb.io/)\n- [Apache Hive](https://hive.apache.org/)\n- [ClickHouse](https://clickhouse.com/)\n- [Snowflake](https://www.snowflake.com/en/)\n- [Oracle](https://www.oracle.com/)\n- [Microsoft SQL Server](https://www.microsoft.com/en-us/sql-server/sql-server-downloads)\n- [BigQuery](https://cloud.google.com/bigquery)\n- [SQLite](https://www.sqlite.org/)\n- [DuckDB](https://duckdb.org/)\n\n\n## Getting started\nSee the [documentation](https://vanna.ai/docs/) for specifics on your desired database, LLM, etc.\n\nIf you want to get a feel for how it works after training, you can try this [Colab notebook](https://vanna.ai/docs/app/).\n\n\n### Install\n```bash\npip install vanna\n```\n\nThere are a number of optional packages that can be installed so see the [documentation](https://vanna.ai/docs/) for more details.\n\n### Import\nSee the [documentation](https://vanna.ai/docs/) if you're customizing the LLM or vector database.\n\n```python\n# The import statement will vary depending on your LLM and vector database. This is an example for OpenAI + ChromaDB\n\nfrom vanna.openai.openai_chat import OpenAI_Chat\nfrom vanna.chromadb.chromadb_vector import ChromaDB_VectorStore\n\nclass MyVanna(ChromaDB_VectorStore, OpenAI_Chat):\n    def __init__(self, config=None):\n        ChromaDB_VectorStore.__init__(self, config=config)\n        OpenAI_Chat.__init__(self, config=config)\n\nvn = MyVanna(config={'api_key': 'sk-...', 'model': 'gpt-4-...'})\n\n# See the documentation for other options\n\n```\n\n\n## Training\nYou may or may not need to run these `vn.train` commands depending on your use case. See the [documentation](https://vanna.ai/docs/) for more details.\n\nThese statements are shown to give you a feel for how it works.\n\n### Train with DDL Statements\nDDL statements contain information about the table names, columns, data types, and relationships in your database.\n\n```python\nvn.train(ddl=\"\"\"\n    CREATE TABLE IF NOT EXISTS my-table (\n        id INT PRIMARY KEY,\n        name VARCHAR(100),\n        age INT\n    )\n\"\"\")\n```\n\n### Train with Documentation\nSometimes you may want to add documentation about your business terminology or definitions.\n\n```python\nvn.train(documentation=\"Our business defines XYZ as ...\")\n```\n\n### Train with SQL\nYou can also add SQL queries to your training data. This is useful if you have some queries already laying around. You can just copy and paste those from your editor to begin generating new SQL.\n\n```python\nvn.train(sql=\"SELECT name, age FROM my-table WHERE name = 'John Doe'\")\n```\n\n\n## Asking questions\n```python\nvn.ask(\"What are the top 10 customers by sales?\")\n```\n\nYou'll get SQL\n```sql\nSELECT c.c_name as customer_name,\n        sum(l.l_extendedprice * (1 - l.l_discount)) as total_sales\nFROM   snowflake_sample_data.tpch_sf1.lineitem l join snowflake_sample_data.tpch_sf1.orders o\n        ON l.l_orderkey = o.o_orderkey join snowflake_sample_data.tpch_sf1.customer c\n        ON o.o_custkey = c.c_custkey\nGROUP BY customer_name\nORDER BY total_sales desc limit 10;\n```\n\nIf you've connected to a database, you'll get the table:\n<div>\n<table border=\"1\" class=\"dataframe\">\n  <thead>\n    <tr style=\"text-align: right;\">\n      <th></th>\n      <th>CUSTOMER_NAME</th>\n      <th>TOTAL_SALES</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <th>0</th>\n      <td>Customer#000143500</td>\n      <td>6757566.0218</td>\n    </tr>\n    <tr>\n      <th>1</th>\n      <td>Customer#000095257</td>\n      <td>6294115.3340</td>\n    </tr>\n    <tr>\n      <th>2</th>\n      <td>Customer#000087115</td>\n      <td>6184649.5176</td>\n    </tr>\n    <tr>\n      <th>3</th>\n      <td>Customer#000131113</td>\n      <td>6080943.8305</td>\n    </tr>\n    <tr>\n      <th>4</th>\n      <td>Customer#000134380</td>\n      <td>6075141.9635</td>\n    </tr>\n    <tr>\n      <th>5</th>\n      <td>Customer#000103834</td>\n      <td>6059770.3232</td>\n    </tr>\n    <tr>\n      <th>6</th>\n      <td>Customer#000069682</td>\n      <td>6057779.0348</td>\n    </tr>\n    <tr>\n      <th>7</th>\n      <td>Customer#000102022</td>\n      <td>6039653.6335</td>\n    </tr>\n    <tr>\n      <th>8</th>\n      <td>Customer#000098587</td>\n      <td>6027021.5855</td>\n    </tr>\n    <tr>\n      <th>9</th>\n      <td>Customer#000064660</td>\n      <td>5905659.6159</td>\n    </tr>\n  </tbody>\n</table>\n</div>\n\nYou'll also get an automated Plotly chart:\n![](img/top-10-customers.png)\n\n## RAG vs. Fine-Tuning\nRAG\n- Portable across LLMs\n- Easy to remove training data if any of it becomes obsolete\n- Much cheaper to run than fine-tuning\n- More future-proof -- if a better LLM comes out, you can just swap it out\n\nFine-Tuning\n- Good if you need to minimize tokens in the prompt\n- Slow to get started\n- Expensive to train and run (generally)\n\n## Why Vanna?\n\n1. **High accuracy on complex datasets.**\n    - Vanna’s capabilities are tied to the training data you give it\n    - More training data means better accuracy for large and complex datasets\n2. **Secure and private.**\n    - Your database contents are never sent to the LLM or the vector database\n    - SQL execution happens in your local environment\n3. **Self learning.**\n    - If using via Jupyter, you can choose to \"auto-train\" it on the queries that were successfully executed\n    - If using via other interfaces, you can have the interface prompt the user to provide feedback on the results\n    - Correct question to SQL pairs are stored for future reference and make the future results more accurate\n4. **Supports any SQL database.**\n    - The package allows you to connect to any SQL database that you can otherwise connect to with Python\n5. **Choose your front end.**\n    - Most people start in a Jupyter Notebook.\n    - Expose to your end users via Slackbot, web app, Streamlit app, or a custom front end.\n\n## Extending Vanna\nVanna is designed to connect to any database, LLM, and vector database. There's a [VannaBase](https://github.com/vanna-ai/vanna/blob/main/src/vanna/base/base.py) abstract base class that defines some basic functionality. The package provides implementations for use with OpenAI and ChromaDB. You can easily extend Vanna to use your own LLM or vector database. See the [documentation](https://vanna.ai/docs/) for more details.\n\n## Vanna in 100 Seconds\n\nhttps://github.com/vanna-ai/vanna/assets/7146154/eb90ee1e-aa05-4740-891a-4fc10e611cab\n\n## More resources\n - [Full Documentation](https://vanna.ai/docs/)\n - [Website](https://vanna.ai)\n - [Discord group for support](https://discord.gg/qUZYKHremx)\n"
  },
  {
    "path": "examples/chromadb_gpu_example.py",
    "content": "\"\"\"\nExample: Using ChromaDB AgentMemory with GPU acceleration\n\nThis example demonstrates how to use ChromaAgentMemory with intelligent\ndevice selection for GPU acceleration when available.\n\"\"\"\n\nfrom vanna.integrations.chromadb import (\n    ChromaAgentMemory,\n    get_device,\n    create_sentence_transformer_embedding_function\n)\n\n\ndef example_default_usage():\n    \"\"\"Example 1: Use default embedding function (no GPU, no sentence-transformers required)\"\"\"\n    print(\"Example 1: Default ChromaDB embedding (CPU-only, no extra dependencies)\")\n\n    memory = ChromaAgentMemory(\n        persist_directory=\"./chroma_memory_default\"\n    )\n\n    print(\"✓ ChromaAgentMemory created with default embedding function\")\n    print()\n\n\ndef example_auto_gpu():\n    \"\"\"Example 2: Automatic GPU detection with SentenceTransformers\"\"\"\n    print(\"Example 2: Automatic GPU detection\")\n\n    # Detect the best available device\n    device = get_device()\n    print(f\"Detected device: {device}\")\n\n    # Create embedding function with automatic device selection\n    embedding_fn = create_sentence_transformer_embedding_function()\n\n    memory = ChromaAgentMemory(\n        persist_directory=\"./chroma_memory_gpu\",\n        embedding_function=embedding_fn\n    )\n\n    print(f\"✓ ChromaAgentMemory created with SentenceTransformer on {device}\")\n    print()\n\n\ndef example_explicit_cuda():\n    \"\"\"Example 3: Explicitly use CUDA\"\"\"\n    print(\"Example 3: Explicitly request CUDA\")\n\n    # Explicitly request CUDA\n    embedding_fn = create_sentence_transformer_embedding_function(device=\"cuda\")\n\n    memory = ChromaAgentMemory(\n        persist_directory=\"./chroma_memory_cuda\",\n        embedding_function=embedding_fn\n    )\n\n    print(\"✓ ChromaAgentMemory created with SentenceTransformer on CUDA\")\n    print()\n\n\ndef example_custom_model_gpu():\n    \"\"\"Example 4: Use a larger model with GPU\"\"\"\n    print(\"Example 4: Custom model with GPU acceleration\")\n\n    # Use a larger, more accurate model with GPU\n    embedding_fn = create_sentence_transformer_embedding_function(\n        model_name=\"sentence-transformers/all-mpnet-base-v2\"\n    )\n\n    memory = ChromaAgentMemory(\n        persist_directory=\"./chroma_memory_large\",\n        embedding_function=embedding_fn\n    )\n\n    print(\"✓ ChromaAgentMemory created with all-mpnet-base-v2 model\")\n    print()\n\n\ndef example_manual_chromadb():\n    \"\"\"Example 5: Manually configure ChromaDB embedding function\"\"\"\n    print(\"Example 5: Manual ChromaDB embedding function configuration\")\n\n    from chromadb.utils import embedding_functions\n\n    # Manually create and configure the embedding function\n    device = get_device()\n    embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(\n        model_name=\"sentence-transformers/all-MiniLM-L6-v2\",\n        device=device\n    )\n\n    memory = ChromaAgentMemory(\n        persist_directory=\"./chroma_memory_manual\",\n        embedding_function=embedding_fn\n    )\n\n    print(f\"✓ ChromaAgentMemory created with manual configuration on {device}\")\n    print()\n\n\nif __name__ == \"__main__\":\n    print(\"=\" * 70)\n    print(\"ChromaDB AgentMemory GPU Acceleration Examples\")\n    print(\"=\" * 70)\n    print()\n\n    # Example 1: Default (no GPU, no sentence-transformers needed)\n    example_default_usage()\n\n    # Examples 2-5 require sentence-transformers to be installed\n    try:\n        import sentence_transformers\n\n        example_auto_gpu()\n\n        # Only run CUDA example if CUDA is available\n        device = get_device()\n        if device == \"cuda\":\n            example_explicit_cuda()\n\n        example_custom_model_gpu()\n        example_manual_chromadb()\n\n    except ImportError:\n        print(\"⚠️  sentence-transformers not installed\")\n        print(\"    Install with: pip install sentence-transformers\")\n        print(\"    Examples 2-5 require this package for GPU acceleration\")\n        print()\n\n    print(\"=\" * 70)\n    print(\"Summary:\")\n    print(\"- Example 1 works without sentence-transformers (CPU only)\")\n    print(\"- Examples 2-5 require sentence-transformers for GPU support\")\n    print(\"- GPU acceleration automatically detected when available\")\n    print(\"=\" * 70)\n"
  },
  {
    "path": "examples/transform_args_example.py",
    "content": "\"\"\"\nExample demonstrating how to use ToolRegistry.transform_args for user-specific\nargument transformation, such as applying row-level security (RLS) to SQL queries.\n\nThis example shows:\n1. Creating a custom ToolRegistry subclass that overrides transform_args\n2. Applying RLS transformation to SQL queries based on user context\n3. Rejecting tool execution when validation fails\n\"\"\"\n\nfrom typing import Union\nfrom pydantic import BaseModel\n\nfrom vanna.core import ToolRegistry\nfrom vanna.core.tool import Tool, ToolContext, ToolRejection, ToolResult\nfrom vanna.core.user import User\n\n\n# Example: SQL execution tool arguments\nclass SQLExecutionArgs(BaseModel):\n    query: str\n    database: str = \"default\"\n\n\nclass SQLExecutionTool(Tool[SQLExecutionArgs]):\n    @property\n    def name(self) -> str:\n        return \"execute_sql\"\n\n    @property\n    def description(self) -> str:\n        return \"Execute a SQL query against the database\"\n\n    def get_args_schema(self):\n        return SQLExecutionArgs\n\n    async def execute(self, context: ToolContext, args: SQLExecutionArgs) -> ToolResult:\n        # Execute the SQL query (implementation not shown)\n        return ToolResult(\n            success=True,\n            result_for_llm=f\"Executed query: {args.query[:50]}...\",\n        )\n\n\nclass RLSToolRegistry(ToolRegistry):\n    \"\"\"Custom ToolRegistry that applies row-level security to SQL queries.\"\"\"\n\n    async def transform_args(\n        self,\n        tool: Tool,\n        args,\n        user: User,\n        context: ToolContext,\n    ) -> Union[SQLExecutionArgs, ToolRejection]:\n        \"\"\"Apply row-level security transformation to SQL queries.\"\"\"\n\n        # Only transform SQL execution tools\n        if tool.name == \"execute_sql\" and isinstance(args, SQLExecutionArgs):\n            original_query = args.query.strip()\n\n            # Example 1: Reject queries that try to access restricted tables\n            if \"restricted_table\" in original_query.lower():\n                return ToolRejection(\n                    reason=\"Access to 'restricted_table' is not permitted for your user group\"\n                )\n\n            # Example 2: Apply RLS by modifying the WHERE clause\n            # This is a simplified example - real RLS would be more sophisticated\n            if \"SELECT\" in original_query.upper() and \"users\" in original_query.lower():\n                # Add a WHERE clause to filter by user's organization\n                user_org_id = user.metadata.get(\"organization_id\")\n\n                if user_org_id:\n                    # Simple RLS: append WHERE clause for organization filtering\n                    if \"WHERE\" in original_query.upper():\n                        transformed_query = original_query.replace(\n                            \"WHERE\",\n                            f\"WHERE organization_id = {user_org_id} AND\",\n                            1\n                        )\n                    else:\n                        # Add WHERE clause before ORDER BY, LIMIT, etc.\n                        transformed_query = original_query.rstrip(\";\")\n                        transformed_query += f\" WHERE organization_id = {user_org_id}\"\n\n                    # Return transformed arguments\n                    return args.model_copy(update={\"query\": transformed_query})\n\n            # Example 3: Validate required parameters\n            if not args.database:\n                return ToolRejection(\n                    reason=\"Database parameter is required for SQL execution\"\n                )\n\n        # For all other tools or if no transformation needed, pass through\n        return args\n\n\n# Usage example\nasync def example_usage():\n    \"\"\"Demonstrate using the RLS-enabled ToolRegistry.\"\"\"\n    from vanna.capabilities.agent_memory import AgentMemory\n\n    # Create registry and register tool\n    registry = RLSToolRegistry()\n    sql_tool = SQLExecutionTool()\n    registry.register_local_tool(sql_tool, access_groups=[])\n\n    # Create a user with organization context\n    user = User(\n        user_id=\"user123\",\n        metadata={\"organization_id\": 42}\n    )\n\n    # Create tool context\n    context = ToolContext(\n        user=user,\n        conversation_id=\"conv123\",\n        request_id=\"req123\",\n        agent_memory=AgentMemory(),\n    )\n\n    # Example 1: Query that will be transformed with RLS\n    from vanna.core.tool import ToolCall\n\n    tool_call = ToolCall(\n        id=\"call1\",\n        name=\"execute_sql\",\n        arguments={\n            \"query\": \"SELECT * FROM users\",\n            \"database\": \"production\"\n        }\n    )\n\n    result = await registry.execute(tool_call, context)\n    print(f\"Result: {result.result_for_llm}\")\n    # The query will be transformed to: SELECT * FROM users WHERE organization_id = 42\n\n    # Example 2: Query that will be rejected\n    tool_call_rejected = ToolCall(\n        id=\"call2\",\n        name=\"execute_sql\",\n        arguments={\n            \"query\": \"SELECT * FROM restricted_table\",\n            \"database\": \"production\"\n        }\n    )\n\n    result = await registry.execute(tool_call_rejected, context)\n    print(f\"Rejected: {result.error}\")\n    # Will return: \"Access to 'restricted_table' is not permitted for your user group\"\n\n\nif __name__ == \"__main__\":\n    import asyncio\n    asyncio.run(example_usage())\n"
  },
  {
    "path": "frontends/webcomponent/.storybook/main.ts",
    "content": "import type { StorybookConfig } from '@storybook/web-components-vite';\n\nconst config: StorybookConfig = {\n  stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],\n  addons: [\n    '@storybook/addon-essentials',\n    '@storybook/addon-actions',\n    '@storybook/addon-controls',\n    '@storybook/addon-docs',\n  ],\n  framework: {\n    name: '@storybook/web-components-vite',\n    options: {},\n  },\n  typescript: {\n    check: false,\n    reactDocgen: 'react-docgen-typescript',\n    reactDocgenTypescriptOptions: {\n      shouldExtractLiteralValuesFromEnum: true,\n      propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),\n    },\n  },\n};\n\nexport default config;"
  },
  {
    "path": "frontends/webcomponent/.storybook/preview-head.html",
    "content": "<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n<link\n  href=\"https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@400;500;600;700&family=Signika:wght@300;400;500;600;700&family=Space+Mono:wght@400;700&display=swap\"\n  rel=\"stylesheet\"\n/>\n"
  },
  {
    "path": "frontends/webcomponent/.storybook/preview.ts",
    "content": "import type { Preview } from '@storybook/web-components';\n\nconst preview: Preview = {\n  parameters: {\n    controls: {\n      matchers: {\n        color: /(background|color)$/i,\n        date: /Date$/i,\n      },\n    },\n    docs: {\n      autodocs: 'tag',\n    },\n  },\n};\n\nexport default preview;"
  },
  {
    "path": "frontends/webcomponent/TEST_README.md",
    "content": "# Vanna Webcomponent Comprehensive Test Suite\n\nThis test suite validates all component types and update patterns in the vanna-webcomponent before pruning unused code.\n\n## Overview\n\nThe test suite consists of:\n- **`test_backend.py`**: Real Python backend that streams all component types\n- **`test-comprehensive.html`**: Browser-based test interface with visual validation\n- **Two test modes**: Rapid (stress test) and Realistic (with delays)\n\n## Quick Start\n\n### 1. Install Dependencies\n\n```bash\ncd submodule/vanna-webcomponent\npip install -r requirements-test.txt\n```\n\n### 2. Build the Webcomponent\n\n```bash\nnpm run build\n```\n\n### 3. Start the Test Backend\n\n```bash\n# Realistic mode (with delays between components)\npython test_backend.py --mode realistic\n\n# Rapid mode (fast stress test)\npython test_backend.py --mode rapid\n```\n\nThe backend will start on `http://localhost:5555` and automatically serve the test page.\n\n### 4. Open Test Interface\n\nSimply open your browser to:\n```\nhttp://localhost:5555\n```\n\nThe test page will load automatically!\n\n### 5. Run the Test\n\n1. Click **\"Run Comprehensive Test\"** button in the sidebar\n2. Watch components render in real-time\n3. Monitor the checklist - items check off as components render\n4. Watch the console log for any errors\n\n## Test Coverage\n\n### Component Types Tested\n\nThe test exercises **all** rich component types with **19 different components**:\n\n#### Primitive Components\n- ✓ Text (with markdown)\n- ✓ Badge\n- ✓ Icon Text\n\n#### Feedback Components\n- ✓ Status Card (with all states: pending, running, completed, failed)\n- ✓ Progress Display (0% → 50% → 100%)\n- ✓ Progress Bar\n- ✓ Status Indicator (with pulse animation)\n- ✓ Notification (info, success, warning, error levels)\n- ✓ Log Viewer (with info, warning, error logs)\n\n#### Data Components\n- ✓ Card (with buttons and actions)\n- ✓ Task List (with status updates)\n- ✓ **DataFrame** (tabular data with search/sort/filter/export)\n- ✓ **Table** (structured data with explicit column definitions)\n- ✓ **Chart** (Plotly charts: bar, line, scatter)\n- ✓ **Code Block** (syntax highlighted code: Python, SQL, etc.)\n\n#### Specialized Components\n- ✓ **Artifact** (HTML/SVG interactive content)\n\n#### Container Components\n- ✓ **Container** (groups components in rows/columns)\n\n#### Interactive Components\n- ✓ Button (single)\n- ✓ Button Group (horizontal/vertical)\n- ✓ Button actions (click → backend response)\n\n#### UI State Updates\n- ✓ Status Bar Update (updates status bar above input)\n- ✓ Task Tracker Update (adds/updates tasks in sidebar)\n- ✓ Chat Input Update (changes placeholder/state)\n\n### Update Operations Tested\n\nFor each component type, the test validates:\n\n1. **Create** (`lifecycle: create`) - Initial component rendering\n2. **Update** (`lifecycle: update`) - Incremental property updates\n3. **Replace** - Full component replacement\n4. **Remove** - Component removal from DOM\n\n### Interactive Features Tested\n\n- **Button Actions**: Clicking buttons sends actions to backend\n- **Action Handling**: Backend receives actions and responds with new components\n- **Round-trip Communication**: Full interaction loop validation\n\n## Test Modes\n\n### Realistic Mode (Default)\n\n```bash\npython test_backend.py --mode realistic\n```\n\n- Includes delays between component updates (0.2-0.5s)\n- Simulates real conversation flow\n- Easier to observe rendering behavior\n- **Recommended for initial validation**\n\n### Rapid Mode\n\n```bash\npython test_backend.py --mode rapid\n```\n\n- Minimal delays (0.05-0.1s)\n- Stress tests rendering performance\n- Validates no race conditions\n- **Use for performance testing**\n\n## Validation Checklist\n\nThe test interface provides real-time validation:\n\n### ✅ Visual Checklist\n- Automatically checks off components as they render\n- Shows 19 component types\n- Green checkmark = successfully rendered\n\n### 📊 Metrics\n- **Components Rendered**: Total unique component types\n- **Updates Processed**: Total number of updates (create + update + replace)\n- **Errors**: Console errors detected\n\n### 🔴 Console Monitor\n- Real-time console log display\n- Errors highlighted in red\n- Warnings in yellow\n- Info messages in blue\n\n### 🟢 Status Indicators\n- **Backend Status**: Green = connected, Red = disconnected\n- **Console Status**: Green = no errors, Red = errors detected\n\n## Using for Webcomponent Pruning\n\nThe test suite is designed to validate that pruning doesn't break functionality:\n\n### Pruning Workflow\n\n1. **Run baseline test**:\n   ```bash\n   python test_backend.py --mode realistic\n\n   # Browser: Open http://localhost:5555 and run test\n   # Verify: All 19 components render, 0 errors\n   ```\n\n2. **Identify cruft to remove**:\n   - Unused imports\n   - Dead code paths\n   - Deprecated components\n   - Development-only utilities\n\n3. **Remove one piece of cruft**:\n   ```bash\n   # Example: Remove unused import from vanna-chat.ts\n   # or delete unused utility file\n   ```\n\n4. **Rebuild**:\n   ```bash\n   npm run build\n   ```\n\n5. **Refresh browser test**:\n   - Press F5 to reload test page\n   - Click \"Run Comprehensive Test\" again\n   - Check console for errors\n   - Verify all 12 components still render\n\n6. **If green → continue; if red → investigate**:\n   - Green (no errors): Commit the change, continue pruning\n   - Red (errors): Revert change, that code was actually needed\n\n7. **Repeat until clean**: Continue removing cruft until webcomponent is minimal\n\n### What to Prune\n\nLook for these common types of cruft:\n\n- ❌ **Unused imports**: Components imported but never used\n- ❌ **Development utilities**: Debug helpers, test mocks in production code\n- ❌ **Deprecated components**: Old component versions no longer referenced\n- ❌ **Unused CSS**: Styles for removed components\n- ❌ **Dead code paths**: Conditional logic that's never executed\n- ❌ **Commented code**: Old implementations that are commented out\n- ❌ **Storybook-only code**: Utilities only used in stories, not production\n\n### What NOT to Prune\n\nBe careful with these:\n\n- ✅ **Base component renderers**: Even if rarely used, may be needed\n- ✅ **ComponentRegistry entries**: Needed for dynamic component lookup\n- ✅ **Shadow DOM utilities**: Required for web components\n- ✅ **Event handlers**: May be used by runtime events\n- ✅ **Type definitions**: Used at compile time even if not runtime\n\n## Customizing the Test\n\n### Add More Component Tests\n\nEdit `test_backend.py` and add new test functions:\n\n```python\nasync def test_my_component(conversation_id: str, request_id: str, mode: str):\n    \"\"\"Test my custom component.\"\"\"\n    my_component = MyComponent(\n        id=str(uuid.uuid4()),\n        # ... component properties\n    )\n    yield await yield_chunk(my_component, conversation_id, request_id)\n    await delay(mode)\n\n# Then add to run_comprehensive_test():\nasync for chunk in test_my_component(conversation_id, request_id, mode):\n    yield chunk\n```\n\n### Modify Test Delays\n\nIn `test_backend.py`, adjust the `delay()` function:\n\n```python\nasync def delay(mode: str, short: float = 0.1, long: float = 0.5):\n    if mode == \"realistic\":\n        await asyncio.sleep(long)  # Adjust long delay here\n    elif mode == \"rapid\":\n        await asyncio.sleep(short)  # Adjust short delay here\n```\n\n### Add Custom Validation\n\nEdit `test-comprehensive.html` and add custom validation logic:\n\n```javascript\n// Add to MutationObserver callback\nconst componentType = node.getAttribute('data-component-type');\nif (componentType === 'my_component') {\n    // Custom validation for my_component\n    console.log('My component rendered!');\n}\n```\n\n## Troubleshooting\n\n### Backend won't start\n\n**Error**: `ModuleNotFoundError: No module named 'vanna'`\n\n**Solution**: Make sure vanna is in the Python path:\n```bash\ncd submodule/vanna-webcomponent\npython test_backend.py  # Already adds ../vanna/src to sys.path\n```\n\n### Frontend shows \"Backend not responding\"\n\n**Solutions**:\n1. Check backend is running: `curl http://localhost:5555/health`\n2. Check CORS is enabled (should be by default)\n3. Verify port 5555 is not in use: `lsof -i :5555`\n\n### Components not rendering\n\n**Check**:\n1. Browser console for errors (F12)\n2. Webcomponent is built: `ls dist/`\n3. Test HTML is loading: `<script type=\"module\" src=\"./dist/index.js\"></script>`\n\n### Test page is blank\n\n**Solutions**:\n1. Check you're serving from the right directory:\n   ```bash\n   cd submodule/vanna-webcomponent\n   python -m http.server 8080\n   ```\n2. Open correct URL: `http://localhost:8080/test-comprehensive.html`\n3. Check browser console for 404 errors\n\n### Checklist not updating\n\nThe checklist tracks components by their `data-component-type` attribute. If components don't have this attribute, they won't be tracked.\n\n**Verify**: Open browser DevTools and inspect rendered components for `data-component-type`.\n\n## Advanced Usage\n\n### Run Backend on Different Port\n\n```bash\npython test_backend.py --port 8000\n```\n\nThen update `test-comprehensive.html`:\n```html\n<vanna-chat\n    api-url=\"http://localhost:8000\"\n    ...\n></vanna-chat>\n```\n\n### Enable Debug Logging\n\nAdd to `test_backend.py`:\n\n```python\nimport logging\nlogging.basicConfig(level=logging.DEBUG)\n```\n\n### Run Type Checking\n\nValidate the backend code with mypy:\n\n```bash\npython -m mypy test_backend.py\n```\n\nThis catches type errors before runtime (e.g., wrong field names in Pydantic models).\n\n### Test Specific Component Only\n\nModify `run_comprehensive_test()` to only run specific tests:\n\n```python\nasync def run_comprehensive_test(conversation_id, request_id, mode):\n    # Comment out tests you don't want to run\n    async for chunk in test_status_card(conversation_id, request_id, mode):\n        yield chunk\n\n    # async for chunk in test_progress_display(...):  # Disabled\n    #     yield chunk\n```\n\n## Architecture\n\n### Backend Flow\n\n1. FastAPI receives POST to `/api/vanna/v2/chat_sse`\n2. `chat_sse()` creates async generator\n3. Generator yields components wrapped in `ChatStreamChunk`\n4. Each chunk serialized to SSE format: `data: {json}\\n\\n`\n5. Stream ends with `data: [DONE]\\n\\n`\n\n### Frontend Flow\n\n1. `<vanna-chat>` web component connects to backend\n2. Opens SSE connection to `/api/vanna/v2/chat_sse`\n3. Receives chunks, parses JSON\n4. `ComponentManager` processes updates\n5. `ComponentRegistry` renders HTML elements\n6. Elements appended to shadow DOM container\n7. MutationObserver detects new components\n8. Checklist updates automatically\n\n### Button Action Flow\n\n1. User clicks button in frontend\n2. Button's `action` property sent as new message\n3. Backend receives message via `/api/vanna/v2/chat_sse` POST\n4. `handle_action_message()` processes action\n5. Response components streamed back\n6. Frontend renders response\n\n## Files\n\n- **`test_backend.py`** - Python FastAPI backend (400 lines)\n- **`test-comprehensive.html`** - Browser test interface (500 lines)\n- **`requirements-test.txt`** - Python dependencies\n- **`TEST_README.md`** - This documentation\n\n## Next Steps\n\nAfter validating the webcomponent with this test suite:\n\n1. **Run baseline test** - Verify all components work before pruning\n2. **Identify cruft** - Find unused code in the webcomponent\n3. **Prune iteratively** - Remove one piece at a time, test after each change\n4. **Commit clean code** - Once pruned, commit the cleaned webcomponent\n5. **Copy to vanna package** - Integrate cleaned webcomponent into vanna Python package\n\n## Support\n\nIf you encounter issues with the test suite:\n\n1. Check this README's Troubleshooting section\n2. Verify all dependencies are installed\n3. Ensure you're in the correct directory\n4. Check browser and terminal console output\n\n---\n\n**Happy Testing!** 🧪\n"
  },
  {
    "path": "frontends/webcomponent/package.json",
    "content": "{\n  \"name\": \"@vanna/webcomponent\",\n  \"version\": \"2.0.0\",\n  \"description\": \"Lit-based web components for Vanna User Agents\",\n  \"main\": \"dist/vanna-components.js\",\n  \"scripts\": {\n    \"sync-version\": \"node scripts/sync-version.js\",\n    \"dev\": \"vite\",\n    \"build\": \"npm run sync-version && tsc && vite build\",\n    \"preview\": \"vite preview\",\n    \"storybook\": \"storybook dev -p 6006\",\n    \"build-storybook\": \"storybook build\",\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"keywords\": [\n    \"vanna\",\n    \"ai\",\n    \"sql\",\n    \"web-components\",\n    \"lit\",\n    \"chat\",\n    \"llm\",\n    \"natural-language\"\n  ],\n  \"author\": \"Zain Hoda <zain@vanna.ai>\",\n  \"license\": \"MIT\",\n  \"type\": \"commonjs\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/vanna-ai/vanna.git\",\n    \"directory\": \"frontends/webcomponent\"\n  },\n  \"homepage\": \"https://github.com/vanna-ai/vanna\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vanna-ai/vanna/issues\"\n  },\n  \"files\": [\n    \"dist\",\n    \"src\"\n  ],\n  \"dependencies\": {\n    \"lit\": \"^3.3.1\",\n    \"plotly.js-dist-min\": \"^3.1.0\"\n  },\n  \"devDependencies\": {\n    \"@storybook/addon-actions\": \"^8.6.14\",\n    \"@storybook/addon-controls\": \"^8.6.14\",\n    \"@storybook/addon-docs\": \"^8.6.14\",\n    \"@storybook/addon-essentials\": \"^8.6.14\",\n    \"@storybook/web-components\": \"^8.6.14\",\n    \"@storybook/web-components-vite\": \"^8.6.14\",\n    \"@types/plotly.js-dist-min\": \"^2.3.4\",\n    \"storybook\": \"^8.6.14\",\n    \"typescript\": \"^5.9.2\",\n    \"vite\": \"^7.1.5\"\n  }\n}\n"
  },
  {
    "path": "frontends/webcomponent/requirements-test.txt",
    "content": "# Test backend requirements for vanna-webcomponent comprehensive testing\n\nfastapi>=0.115.0\nuvicorn[standard]>=0.32.0\npydantic>=2.0.0\n\n# Note: The vanna package itself will be imported from ../vanna/src\n# No need to install it separately for local testing\n"
  },
  {
    "path": "frontends/webcomponent/scripts/sync-version.js",
    "content": "/**\n * Sync version from pyproject.toml to package.json\n *\n * This ensures the webcomponent version always matches the Python package version.\n * Single source of truth: pyproject.toml\n *\n * Usage: node scripts/sync-version.js\n */\n\nconst fs = require('fs');\nconst path = require('path');\n\n// Paths relative to this script\nconst PYPROJECT_PATH = path.join(__dirname, '../../../pyproject.toml');\nconst PACKAGE_JSON_PATH = path.join(__dirname, '../package.json');\n\nfunction extractVersionFromPyproject(content) {\n  // Match: version = \"2.0.0\"\n  const match = content.match(/^version\\s*=\\s*\"([^\"]+)\"/m);\n  if (!match) {\n    throw new Error('Could not find version in pyproject.toml');\n  }\n  return match[1];\n}\n\nfunction updatePackageJsonVersion(packageJsonPath, newVersion) {\n  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));\n  const oldVersion = packageJson.version;\n\n  packageJson.version = newVersion;\n\n  fs.writeFileSync(\n    packageJsonPath,\n    JSON.stringify(packageJson, null, 2) + '\\n',\n    'utf8'\n  );\n\n  return { oldVersion, newVersion };\n}\n\nfunction main() {\n  try {\n    // Read pyproject.toml\n    const pyprojectContent = fs.readFileSync(PYPROJECT_PATH, 'utf8');\n    const version = extractVersionFromPyproject(pyprojectContent);\n\n    // Update package.json\n    const { oldVersion, newVersion } = updatePackageJsonVersion(PACKAGE_JSON_PATH, version);\n\n    if (oldVersion !== newVersion) {\n      console.log(`✓ Version synced: ${oldVersion} → ${newVersion}`);\n    } else {\n      console.log(`✓ Version already in sync: ${newVersion}`);\n    }\n\n    process.exit(0);\n  } catch (error) {\n    console.error(`✗ Version sync failed: ${error.message}`);\n    process.exit(1);\n  }\n}\n\nmain();\n"
  },
  {
    "path": "frontends/webcomponent/src/components/button.stories.ts",
    "content": "import type { Meta, StoryObj } from '@storybook/web-components';\nimport { ComponentManager, ComponentUpdate } from './rich-component-system';\nimport { vannaDesignTokens } from '../styles/vanna-design-tokens.js';\n\nconst meta: Meta = {\n  title: 'Rich Components/Buttons',\n  parameters: {\n    layout: 'padded',\n    backgrounds: {\n      default: 'dark',\n      values: [\n        { name: 'light', value: '#f5f7fa' },\n        { name: 'dark', value: 'rgb(11, 15, 25)' },\n      ],\n    },\n  },\n};\n\nexport default meta;\ntype Story = StoryObj;\n\nconst ensureTokenStyles = () => {\n  if (document.getElementById('vanna-token-style')) {\n    return;\n  }\n\n  const style = document.createElement('style');\n  style.id = 'vanna-token-style';\n  style.textContent = vannaDesignTokens.cssText.replace(/:host/g, '.vanna-tokens');\n  document.head.appendChild(style);\n};\n\nconst createContainer = () => {\n  ensureTokenStyles();\n\n  const container = document.createElement('div');\n  container.className = 'vanna-tokens';\n  container.style.cssText = `\n    padding: var(--vanna-space-5, 20px);\n    max-width: 800px;\n    margin: 0 auto;\n    background: var(--vanna-background-default);\n    border-radius: var(--vanna-border-radius-lg);\n    box-shadow: var(--vanna-shadow-md);\n  `;\n\n  return container;\n};\n\nconst createManager = (container: HTMLElement) => new ComponentManager(container);\n\nconst renderComponent = (manager: ComponentManager, component: any) => {\n  const update: ComponentUpdate = {\n    operation: 'create',\n    target_id: component.id,\n    component,\n    timestamp: new Date().toISOString(),\n  } as ComponentUpdate;\n\n  manager.processUpdate(update);\n};\n\nconst withDefaults = (component: any) => ({\n  layout: { position: 'append', size: {}, z_index: 0, classes: [] },\n  theme: {},\n  lifecycle: 'create',\n  ...component,\n});\n\nconst addMockVannaChat = (container: HTMLElement) => {\n  // Create a mock vanna-chat element with sendMessage method\n  const mockVannaChat = document.createElement('div');\n  mockVannaChat.setAttribute('id', 'mock-vanna-chat');\n\n  // Store the original querySelector\n  const originalQuerySelector = document.querySelector.bind(document);\n\n  // Override querySelector to return our mock when looking for vanna-chat\n  document.querySelector = function(selector: string) {\n    if (selector === 'vanna-chat') {\n      return mockVannaChat as any;\n    }\n    return originalQuerySelector(selector);\n  } as any;\n\n  // Add sendMessage method that logs to console and shows in UI\n  (mockVannaChat as any).sendMessage = (message: string) => {\n    console.log('📤 Button clicked - Message:', message);\n\n    // Show a visual feedback in the storybook\n    const feedback = document.createElement('div');\n    feedback.style.cssText = `\n      position: fixed;\n      top: 20px;\n      right: 20px;\n      background: #4CAF50;\n      color: white;\n      padding: 12px 20px;\n      border-radius: 8px;\n      box-shadow: 0 4px 6px rgba(0,0,0,0.3);\n      font-family: monospace;\n      z-index: 10000;\n      animation: slideIn 0.3s ease-out;\n    `;\n    feedback.textContent = `Message sent: ${message}`;\n\n    // Add animation\n    const style = document.createElement('style');\n    style.textContent = `\n      @keyframes slideIn {\n        from {\n          transform: translateX(100%);\n          opacity: 0;\n        }\n        to {\n          transform: translateX(0);\n          opacity: 1;\n        }\n      }\n    `;\n    document.head.appendChild(style);\n\n    document.body.appendChild(feedback);\n\n    setTimeout(() => {\n      feedback.style.opacity = '0';\n      feedback.style.transition = 'opacity 0.3s ease-out';\n      setTimeout(() => feedback.remove(), 300);\n    }, 2000);\n  };\n\n  container.appendChild(mockVannaChat);\n  return mockVannaChat;\n};\n\nexport const SingleButtons: Story = {\n  render: () => {\n    const container = createContainer();\n    const manager = createManager(container);\n    addMockVannaChat(container);\n\n    // Add title\n    const title = document.createElement('h2');\n    title.textContent = 'Single Button Components';\n    title.style.cssText = 'margin-bottom: 20px; color: var(--vanna-text-primary);';\n    container.appendChild(title);\n\n    const buttons = [\n      withDefaults({\n        id: 'primary-button',\n        type: 'button',\n        data: {\n          label: 'Primary Action',\n          action: 'primary_action',\n          variant: 'primary',\n          size: 'medium',\n        },\n      }),\n      withDefaults({\n        id: 'secondary-button',\n        type: 'button',\n        data: {\n          label: 'Save Draft',\n          action: 'save_draft',\n          variant: 'secondary',\n          size: 'medium',\n          icon: '💾',\n          icon_position: 'left',\n        },\n      }),\n      withDefaults({\n        id: 'success-button',\n        type: 'button',\n        data: {\n          label: 'Approve',\n          action: 'approve',\n          variant: 'success',\n          size: 'medium',\n          icon: '✓',\n        },\n      }),\n      withDefaults({\n        id: 'warning-button',\n        type: 'button',\n        data: {\n          label: 'Caution',\n          action: 'warning',\n          variant: 'warning',\n          size: 'medium',\n          icon: '⚠️',\n        },\n      }),\n      withDefaults({\n        id: 'error-button',\n        type: 'button',\n        data: {\n          label: 'Delete',\n          action: 'delete',\n          variant: 'error',\n          size: 'medium',\n          icon: '🗑️',\n        },\n      }),\n      withDefaults({\n        id: 'ghost-button',\n        type: 'button',\n        data: {\n          label: 'Ghost Style',\n          action: 'ghost',\n          variant: 'ghost',\n          icon: '👻',\n        },\n      }),\n      withDefaults({\n        id: 'link-button',\n        type: 'button',\n        data: {\n          label: 'Learn More',\n          action: 'learn_more',\n          variant: 'link',\n        },\n      }),\n      withDefaults({\n        id: 'loading-button',\n        type: 'button',\n        data: {\n          label: 'Processing...',\n          action: 'loading',\n          variant: 'primary',\n          loading: true,\n        },\n      }),\n      withDefaults({\n        id: 'disabled-button',\n        type: 'button',\n        data: {\n          label: 'Disabled',\n          action: 'disabled',\n          variant: 'secondary',\n          disabled: true,\n        },\n      }),\n    ];\n\n    buttons.forEach((component) => {\n      renderComponent(manager, component);\n      // Add some spacing\n      const spacer = document.createElement('div');\n      spacer.style.height = '12px';\n      container.appendChild(spacer);\n    });\n\n    // Add instruction\n    const instruction = document.createElement('p');\n    instruction.textContent = 'Click any button to see the message it sends (wrapped in square brackets)';\n    instruction.style.cssText = 'margin-top: 20px; color: var(--vanna-text-secondary); font-style: italic;';\n    container.appendChild(instruction);\n\n    return container;\n  },\n};\n\nexport const ButtonSizes: Story = {\n  render: () => {\n    const container = createContainer();\n    const manager = createManager(container);\n    addMockVannaChat(container);\n\n    const title = document.createElement('h2');\n    title.textContent = 'Button Sizes';\n    title.style.cssText = 'margin-bottom: 20px; color: var(--vanna-text-primary);';\n    container.appendChild(title);\n\n    const sizes = ['small', 'medium', 'large'];\n\n    sizes.forEach((size) => {\n      const button = withDefaults({\n        id: `button-${size}`,\n        type: 'button',\n        data: {\n          label: `${size.charAt(0).toUpperCase() + size.slice(1)} Button`,\n          action: `${size}_action`,\n          variant: 'primary',\n          size,\n          icon: '⭐',\n        },\n      });\n\n      renderComponent(manager, button);\n\n      const spacer = document.createElement('div');\n      spacer.style.height = '12px';\n      container.appendChild(spacer);\n    });\n\n    return container;\n  },\n};\n\nexport const ButtonGroups: Story = {\n  render: () => {\n    const container = createContainer();\n    const manager = createManager(container);\n    addMockVannaChat(container);\n\n    const title = document.createElement('h2');\n    title.textContent = 'Button Group Components';\n    title.style.cssText = 'margin-bottom: 20px; color: var(--vanna-text-primary);';\n    container.appendChild(title);\n\n    // Horizontal action group\n    const actionGroup = withDefaults({\n      id: 'action-group',\n      type: 'button_group',\n      data: {\n        buttons: [\n          {\n            label: 'Accept',\n            action: 'accept',\n            variant: 'success',\n            icon: '✓',\n          },\n          {\n            label: 'Reject',\n            action: 'reject',\n            variant: 'error',\n            icon: '✗',\n          },\n          {\n            label: 'Cancel',\n            action: 'cancel',\n            variant: 'secondary',\n          },\n        ],\n        orientation: 'horizontal',\n        spacing: 'medium',\n        align: 'left',\n      },\n    });\n\n    const sectionTitle1 = document.createElement('h3');\n    sectionTitle1.textContent = 'Horizontal Action Group';\n    sectionTitle1.style.cssText = 'margin: 20px 0 10px 0; color: var(--vanna-text-primary); font-size: 16px;';\n    container.appendChild(sectionTitle1);\n    renderComponent(manager, actionGroup);\n\n    // Centered navigation\n    const navigationGroup = withDefaults({\n      id: 'navigation-group',\n      type: 'button_group',\n      data: {\n        buttons: [\n          {\n            label: 'Back',\n            action: 'back',\n            variant: 'ghost',\n            icon: '←',\n          },\n          {\n            label: 'Continue',\n            action: 'continue',\n            variant: 'primary',\n            icon: '→',\n            icon_position: 'right',\n          },\n        ],\n        orientation: 'horizontal',\n        spacing: 'large',\n        align: 'center',\n      },\n    });\n\n    const sectionTitle2 = document.createElement('h3');\n    sectionTitle2.textContent = 'Centered Navigation';\n    sectionTitle2.style.cssText = 'margin: 20px 0 10px 0; color: var(--vanna-text-primary); font-size: 16px;';\n    container.appendChild(sectionTitle2);\n    renderComponent(manager, navigationGroup);\n\n    // Vertical options\n    const verticalGroup = withDefaults({\n      id: 'vertical-group',\n      type: 'button_group',\n      data: {\n        buttons: [\n          { label: 'Option 1', action: 'option1', variant: 'secondary' },\n          { label: 'Option 2', action: 'option2', variant: 'secondary' },\n          { label: 'Option 3', action: 'option3', variant: 'secondary' },\n        ],\n        orientation: 'vertical',\n        spacing: 'small',\n        align: 'left',\n      },\n    });\n\n    const sectionTitle3 = document.createElement('h3');\n    sectionTitle3.textContent = 'Vertical Options';\n    sectionTitle3.style.cssText = 'margin: 20px 0 10px 0; color: var(--vanna-text-primary); font-size: 16px;';\n    container.appendChild(sectionTitle3);\n    renderComponent(manager, verticalGroup);\n\n    // Toolbar\n    const toolbarGroup = withDefaults({\n      id: 'toolbar-group',\n      type: 'button_group',\n      data: {\n        buttons: [\n          {\n            label: 'New',\n            action: 'new',\n            variant: 'primary',\n            icon: '➕',\n            size: 'small',\n          },\n          {\n            label: 'Edit',\n            action: 'edit',\n            variant: 'secondary',\n            icon: '✏️',\n            size: 'small',\n          },\n          {\n            label: 'Delete',\n            action: 'delete',\n            variant: 'error',\n            icon: '🗑️',\n            size: 'small',\n          },\n          {\n            label: 'Share',\n            action: 'share',\n            variant: 'ghost',\n            icon: '🔗',\n            size: 'small',\n          },\n        ],\n        orientation: 'horizontal',\n        spacing: 'small',\n        align: 'left',\n      },\n    });\n\n    const sectionTitle4 = document.createElement('h3');\n    sectionTitle4.textContent = 'Toolbar (Small Buttons)';\n    sectionTitle4.style.cssText = 'margin: 20px 0 10px 0; color: var(--vanna-text-primary); font-size: 16px;';\n    container.appendChild(sectionTitle4);\n    renderComponent(manager, toolbarGroup);\n\n    // Full width confirmation\n    const confirmationGroup = withDefaults({\n      id: 'confirmation-group',\n      type: 'button_group',\n      data: {\n        buttons: [\n          { label: 'Yes', action: 'yes', variant: 'success' },\n          { label: 'No', action: 'no', variant: 'error' },\n        ],\n        orientation: 'horizontal',\n        spacing: 'medium',\n        align: 'space-between',\n        full_width: true,\n      },\n    });\n\n    const sectionTitle5 = document.createElement('h3');\n    sectionTitle5.textContent = 'Full Width Confirmation';\n    sectionTitle5.style.cssText = 'margin: 20px 0 10px 0; color: var(--vanna-text-primary); font-size: 16px;';\n    container.appendChild(sectionTitle5);\n    renderComponent(manager, confirmationGroup);\n\n    // Add instruction\n    const instruction = document.createElement('p');\n    instruction.textContent = 'Click any button in the groups to see the message it sends';\n    instruction.style.cssText = 'margin-top: 20px; color: var(--vanna-text-secondary); font-style: italic;';\n    container.appendChild(instruction);\n\n    return container;\n  },\n};\n\nexport const InteractiveDemo: Story = {\n  render: () => {\n    const container = createContainer();\n    const manager = createManager(container);\n    addMockVannaChat(container);\n\n    const title = document.createElement('h2');\n    title.textContent = 'Interactive Button Demo';\n    title.style.cssText = 'margin-bottom: 20px; color: var(--vanna-text-primary);';\n    container.appendChild(title);\n\n    const description = document.createElement('p');\n    description.textContent = 'This demo shows how buttons send messages with their labels wrapped in square brackets.';\n    description.style.cssText = 'margin-bottom: 20px; color: var(--vanna-text-secondary);';\n    container.appendChild(description);\n\n    // Simple choice buttons\n    const choiceGroup = withDefaults({\n      id: 'choice-group',\n      type: 'button_group',\n      data: {\n        buttons: [\n          { label: 'Okay', action: 'okay', variant: 'primary' },\n          { label: 'Not now', action: 'not_now', variant: 'secondary' },\n          { label: 'Never', action: 'never', variant: 'ghost' },\n        ],\n        orientation: 'horizontal',\n        spacing: 'medium',\n        align: 'center',\n      },\n    });\n\n    renderComponent(manager, choiceGroup);\n\n    const codeExample = document.createElement('pre');\n    codeExample.textContent = `// When you click \"Okay\", the message sent is: [Okay]\n// When you click \"Not now\", the message sent is: [Not now]\n// When you click \"Never\", the message sent is: [Never]`;\n    codeExample.style.cssText = `\n      margin-top: 20px;\n      padding: 12px;\n      background: rgba(0, 0, 0, 0.3);\n      border-radius: 6px;\n      color: #a0aec0;\n      font-size: 12px;\n      font-family: 'Courier New', monospace;\n      overflow-x: auto;\n    `;\n    container.appendChild(codeExample);\n\n    return container;\n  },\n};\n"
  },
  {
    "path": "frontends/webcomponent/src/components/dataframe-component.stories.ts",
    "content": "import type { Meta, StoryObj } from '@storybook/web-components';\nimport { ComponentManager, ComponentUpdate } from './rich-component-system';\nimport { vannaDesignTokens } from '../styles/vanna-design-tokens.js';\nimport { richComponentStyleText } from '../styles/rich-component-styles.js';\n\nconst meta: Meta = {\n  title: 'Rich Components/DataFrame',\n  parameters: {\n    layout: 'padded',\n    backgrounds: {\n      default: 'light',\n      values: [\n        { name: 'light', value: '#f5f7fa' },\n        { name: 'dark', value: 'rgb(11, 15, 25)' },\n      ],\n    },\n  },\n  argTypes: {\n    theme: {\n      control: { type: 'select' },\n      options: ['light', 'dark'],\n    },\n    striped: {\n      control: { type: 'boolean' },\n    },\n    bordered: {\n      control: { type: 'boolean' },\n    },\n    compact: {\n      control: { type: 'boolean' },\n    },\n    searchable: {\n      control: { type: 'boolean' },\n    },\n    sortable: {\n      control: { type: 'boolean' },\n    },\n    exportable: {\n      control: { type: 'boolean' },\n    },\n  },\n};\n\nexport default meta;\ntype Story = StoryObj;\n\nconst ensureTokenStyles = () => {\n  if (document.getElementById('vanna-token-style')) {\n    return;\n  }\n\n  const style = document.createElement('style');\n  style.id = 'vanna-token-style';\n  style.textContent = vannaDesignTokens.cssText.replace(/:host/g, '.vanna-tokens');\n  document.head.appendChild(style);\n};\n\nconst ensureRichComponentStyles = () => {\n  if (document.getElementById('vanna-rich-component-styles')) {\n    return;\n  }\n\n  const style = document.createElement('style');\n  style.id = 'vanna-rich-component-styles';\n  style.textContent = richComponentStyleText;\n  document.head.appendChild(style);\n};\n\nconst createContainer = () => {\n  ensureTokenStyles();\n  ensureRichComponentStyles();\n\n  const container = document.createElement('div');\n  container.className = 'vanna-tokens';\n  container.style.cssText = `\n    padding: var(--vanna-space-5, 20px);\n    max-width: 1200px;\n    margin: 0 auto;\n    background: var(--vanna-background-default, #0b0f19);\n    border-radius: var(--vanna-border-radius-lg, 8px);\n    box-shadow: var(--vanna-shadow-md, 0 4px 6px rgba(0, 0, 0, 0.1));\n    color: var(--vanna-foreground-default, #ffffff);\n  `;\n\n  // Add some additional DataFrame-specific debugging styles\n  const additionalStyles = document.createElement('style');\n  additionalStyles.textContent = `\n    /* Ensure DataFrame styles are applied with higher specificity */\n    .vanna-tokens {\n      font-family: var(--vanna-font-family-default, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif) !important;\n    }\n\n    .vanna-tokens .rich-dataframe {\n      background: var(--vanna-background-default, #0b0f19) !important;\n      border: 1px solid var(--vanna-outline-default, #333) !important;\n      border-radius: var(--vanna-border-radius-lg, 8px) !important;\n      overflow: hidden !important;\n      font-family: var(--vanna-font-family-default, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif) !important;\n    }\n\n    .vanna-tokens .dataframe-table {\n      width: 100% !important;\n      border-collapse: collapse !important;\n      font-size: 0.875rem !important;\n      color: var(--vanna-foreground-default, #ffffff) !important;\n      font-family: var(--vanna-font-family-default, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif) !important;\n    }\n\n    .vanna-tokens .dataframe-table th {\n      background: var(--vanna-background-higher, #1a1f2e) !important;\n      color: var(--vanna-foreground-default, #ffffff) !important;\n      font-weight: 600 !important;\n      text-align: left !important;\n      padding: 12px 16px !important;\n      border-bottom: 2px solid var(--vanna-outline-default, #333) !important;\n      font-family: var(--vanna-font-family-default, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif) !important;\n    }\n\n    .vanna-tokens .dataframe-table td {\n      padding: 12px 16px !important;\n      border-bottom: 1px solid var(--vanna-outline-dimmer, #222) !important;\n      color: var(--vanna-foreground-default, #ffffff) !important;\n      font-family: var(--vanna-font-family-default, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif) !important;\n    }\n\n    .vanna-tokens .dataframe-table.striped tbody tr:nth-child(even) {\n      background: rgba(255, 255, 255, 0.02) !important;\n    }\n\n    .vanna-tokens .dataframe-header {\n      padding: 16px 20px !important;\n      background: var(--vanna-background-higher, #1a1f2e) !important;\n      border-bottom: 1px solid var(--vanna-outline-default, #333) !important;\n      font-family: var(--vanna-font-family-default, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif) !important;\n    }\n\n    .vanna-tokens .dataframe-title {\n      font-family: var(--vanna-font-family-default, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif) !important;\n      color: var(--vanna-foreground-default, #ffffff) !important;\n      font-weight: 600 !important;\n    }\n\n    .vanna-tokens .dataframe-description {\n      font-family: var(--vanna-font-family-default, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif) !important;\n      color: var(--vanna-foreground-dimmer, #b1bac4) !important;\n    }\n\n    .vanna-tokens .dataframe-actions {\n      padding: 12px 20px !important;\n      background: var(--vanna-background-default, #0b0f19) !important;\n      border-bottom: 1px solid var(--vanna-outline-dimmer, #222) !important;\n      display: flex !important;\n      justify-content: space-between !important;\n      align-items: center !important;\n      gap: 12px !important;\n    }\n\n    .vanna-tokens .search-input {\n      width: 100% !important;\n      padding: 8px 12px !important;\n      border: 1px solid var(--vanna-outline-default, #333) !important;\n      border-radius: 6px !important;\n      background: var(--vanna-background-default, #0b0f19) !important;\n      color: var(--vanna-foreground-default, #ffffff) !important;\n      font-size: 0.875rem !important;\n      font-family: var(--vanna-font-family-default, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif) !important;\n    }\n\n    .vanna-tokens .export-btn {\n      padding: 8px 12px !important;\n      border: 1px solid var(--vanna-outline-default, #333) !important;\n      border-radius: 6px !important;\n      background: var(--vanna-background-default, #0b0f19) !important;\n      color: var(--vanna-foreground-default, #ffffff) !important;\n      cursor: pointer !important;\n      font-size: 0.875rem !important;\n      font-family: var(--vanna-font-family-default, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif) !important;\n    }\n  `;\n  document.head.appendChild(additionalStyles);\n\n  return container;\n};\n\nconst createManager = (container: HTMLElement) => new ComponentManager(container);\n\nconst renderComponent = (manager: ComponentManager, component: any) => {\n  const update: ComponentUpdate = {\n    operation: 'create',\n    target_id: component.id,\n    component,\n    timestamp: new Date().toISOString(),\n  } as ComponentUpdate;\n\n  manager.processUpdate(update);\n};\n\nconst withDefaults = (component: any) => ({\n  layout: { position: 'append', size: {}, z_index: 0, classes: [] },\n  theme: {},\n  lifecycle: 'create',\n  timestamp: new Date().toISOString(),\n  visible: true,\n  interactive: false,\n  children: [],\n  ...component,\n});\n\n// Sample data sets\nconst employeeData = [\n  { id: 1, name: 'Alice Johnson', email: 'alice@example.com', age: 28, city: 'New York', salary: 75000, active: true, department: 'Engineering' },\n  { id: 2, name: 'Bob Smith', email: 'bob@example.com', age: 34, city: 'San Francisco', salary: 85000, active: true, department: 'Product' },\n  { id: 3, name: 'Carol Davis', email: 'carol@example.com', age: 29, city: 'Chicago', salary: 70000, active: false, department: 'Design' },\n  { id: 4, name: 'David Wilson', email: 'david@example.com', age: 42, city: 'Austin', salary: 90000, active: true, department: 'Engineering' },\n  { id: 5, name: 'Eve Brown', email: 'eve@example.com', age: 31, city: 'Seattle', salary: 80000, active: true, department: 'Marketing' },\n  { id: 6, name: 'Frank Miller', email: 'frank@example.com', age: 38, city: 'Boston', salary: 95000, active: false, department: 'Sales' },\n  { id: 7, name: 'Grace Lee', email: 'grace@example.com', age: 26, city: 'Denver', salary: 65000, active: true, department: 'HR' },\n  { id: 8, name: 'Henry Taylor', email: 'henry@example.com', age: 33, city: 'Portland', salary: 72000, active: true, department: 'Engineering' },\n  { id: 9, name: 'Ivy Chen', email: 'ivy@example.com', age: 27, city: 'Los Angeles', salary: 78000, active: true, department: 'Product' },\n  { id: 10, name: 'Jack Anderson', email: 'jack@example.com', age: 35, city: 'Miami', salary: 82000, active: false, department: 'Finance' },\n];\n\nconst sqlQueryData = [\n  { TrackId: 1, Name: 'For Those About To Rock (We Salute You)', AlbumId: 1, MediaTypeId: 1, GenreId: 1, Composer: 'Angus Young, Malcolm Young, Brian Johnson', Milliseconds: 343719, Bytes: 11170334, UnitPrice: 0.99 },\n  { TrackId: 2, Name: 'Balls to the Wall', AlbumId: 2, MediaTypeId: 2, GenreId: 1, Composer: null, Milliseconds: 342562, Bytes: 5510424, UnitPrice: 0.99 },\n  { TrackId: 3, Name: 'Fast As a Shark', AlbumId: 3, MediaTypeId: 2, GenreId: 1, Composer: 'F. Baltes, S. Kaufman, U. Dirkscneider & W. Hoffman', Milliseconds: 230619, Bytes: 3990994, UnitPrice: 0.99 },\n  { TrackId: 4, Name: 'Restless and Wild', AlbumId: 3, MediaTypeId: 2, GenreId: 1, Composer: 'F. Baltes, R.A. Smith-Diesel, S. Kaufman, U. Dirkscneider & W. Hoffman', Milliseconds: 252051, Bytes: 4331779, UnitPrice: 0.99 },\n  { TrackId: 5, Name: 'Princess of the Dawn', AlbumId: 3, MediaTypeId: 2, GenreId: 1, Composer: 'Deaffy & R.A. Smith-Diesel', Milliseconds: 375418, Bytes: 6290521, UnitPrice: 0.99 },\n  { TrackId: 6, Name: 'Put The Finger On You', AlbumId: 1, MediaTypeId: 1, GenreId: 1, Composer: 'Angus Young, Malcolm Young, Brian Johnson', Milliseconds: 205662, Bytes: 6713451, UnitPrice: 0.99 },\n  { TrackId: 7, Name: \"Let's Get It Up\", AlbumId: 1, MediaTypeId: 1, GenreId: 1, Composer: 'Angus Young, Malcolm Young, Brian Johnson', Milliseconds: 233926, Bytes: 7636561, UnitPrice: 0.99 },\n  { TrackId: 8, Name: 'Inject The Venom', AlbumId: 1, MediaTypeId: 1, GenreId: 1, Composer: 'Angus Young, Malcolm Young, Brian Johnson', Milliseconds: 210834, Bytes: 6852860, UnitPrice: 0.99 },\n];\n\nexport const BasicDataFrame: Story = {\n  render: (args) => {\n    const container = createContainer();\n    const manager = createManager(container);\n\n    const component = withDefaults({\n      id: 'basic-dataframe',\n      type: 'dataframe',\n      data: {\n        data: employeeData.slice(0, 5),\n        columns: ['id', 'name', 'email', 'age', 'city', 'department'],\n        title: 'Employee Records',\n        description: 'Basic employee data with essential information',\n        row_count: 5,\n        column_count: 6,\n        striped: args.striped ?? true,\n        bordered: args.bordered ?? true,\n        compact: args.compact ?? false,\n        searchable: args.searchable ?? false,\n        sortable: args.sortable ?? false,\n        exportable: args.exportable ?? false,\n        column_types: {\n          id: 'number',\n          name: 'string',\n          email: 'string',\n          age: 'number',\n          city: 'string',\n          department: 'string'\n        }\n      },\n    });\n\n    renderComponent(manager, component);\n    return container;\n  },\n  args: {\n    striped: true,\n    bordered: true,\n    compact: false,\n    searchable: false,\n    sortable: false,\n    exportable: false,\n  },\n};\n\nexport const InteractiveDataFrame: Story = {\n  render: (args) => {\n    const container = createContainer();\n    const manager = createManager(container);\n\n    const component = withDefaults({\n      id: 'interactive-dataframe',\n      type: 'dataframe',\n      data: {\n        data: employeeData,\n        columns: ['id', 'name', 'email', 'age', 'city', 'salary', 'active', 'department'],\n        title: 'Interactive Employee Database',\n        description: 'Full dataset with search, sort, and export functionality',\n        row_count: employeeData.length,\n        column_count: 8,\n        striped: args.striped ?? true,\n        bordered: args.bordered ?? true,\n        compact: args.compact ?? false,\n        searchable: args.searchable ?? true,\n        sortable: args.sortable ?? true,\n        exportable: args.exportable ?? true,\n        max_rows_displayed: 8,\n        column_types: {\n          id: 'number',\n          name: 'string',\n          email: 'string',\n          age: 'number',\n          city: 'string',\n          salary: 'number',\n          active: 'boolean',\n          department: 'string'\n        }\n      },\n    });\n\n    renderComponent(manager, component);\n    return container;\n  },\n  args: {\n    striped: true,\n    bordered: true,\n    compact: false,\n    searchable: true,\n    sortable: true,\n    exportable: true,\n  },\n};\n\nexport const SQLQueryResults: Story = {\n  render: (args) => {\n    const container = createContainer();\n    const manager = createManager(container);\n\n    const component = withDefaults({\n      id: 'sql-dataframe',\n      type: 'dataframe',\n      data: {\n        data: sqlQueryData,\n        columns: ['TrackId', 'Name', 'AlbumId', 'MediaTypeId', 'GenreId', 'Composer', 'Milliseconds', 'Bytes', 'UnitPrice'],\n        title: 'SQL Query Results',\n        description: 'SELECT * FROM Track LIMIT 8',\n        row_count: sqlQueryData.length,\n        column_count: 9,\n        striped: args.striped ?? true,\n        bordered: args.bordered ?? true,\n        compact: args.compact ?? false,\n        searchable: args.searchable ?? true,\n        sortable: args.sortable ?? true,\n        exportable: args.exportable ?? true,\n        column_types: {\n          TrackId: 'number',\n          Name: 'string',\n          AlbumId: 'number',\n          MediaTypeId: 'number',\n          GenreId: 'number',\n          Composer: 'string',\n          Milliseconds: 'number',\n          Bytes: 'number',\n          UnitPrice: 'number'\n        }\n      },\n    });\n\n    renderComponent(manager, component);\n    return container;\n  },\n  args: {\n    striped: true,\n    bordered: true,\n    compact: false,\n    searchable: true,\n    sortable: true,\n    exportable: true,\n  },\n};\n\nexport const CompactView: Story = {\n  render: (args) => {\n    const container = createContainer();\n    const manager = createManager(container);\n\n    const component = withDefaults({\n      id: 'compact-dataframe',\n      type: 'dataframe',\n      data: {\n        data: employeeData.slice(0, 6),\n        columns: ['id', 'name', 'city', 'active'],\n        title: 'Compact Employee View',\n        description: 'Space-efficient display with essential columns only',\n        row_count: 6,\n        column_count: 4,\n        striped: args.striped ?? true,\n        bordered: args.bordered ?? false,\n        compact: args.compact ?? true,\n        searchable: args.searchable ?? false,\n        sortable: args.sortable ?? true,\n        exportable: args.exportable ?? false,\n        column_types: {\n          id: 'number',\n          name: 'string',\n          city: 'string',\n          active: 'boolean'\n        }\n      },\n    });\n\n    renderComponent(manager, component);\n    return container;\n  },\n  args: {\n    striped: true,\n    bordered: false,\n    compact: true,\n    searchable: false,\n    sortable: true,\n    exportable: false,\n  },\n};\n\nexport const EmptyDataFrame: Story = {\n  render: () => {\n    const container = createContainer();\n    const manager = createManager(container);\n\n    const component = withDefaults({\n      id: 'empty-dataframe',\n      type: 'dataframe',\n      data: {\n        data: [],\n        columns: [],\n        title: 'No Data Available',\n        description: 'This dataset contains no records',\n        row_count: 0,\n        column_count: 0,\n      },\n    });\n\n    renderComponent(manager, component);\n    return container;\n  },\n};\n\nexport const LargeDataset: Story = {\n  render: (args) => {\n    const container = createContainer();\n    const manager = createManager(container);\n\n    // Generate a larger dataset\n    const largeData = Array.from({ length: 50 }, (_, i) => ({\n      id: i + 1,\n      name: `User ${i + 1}`,\n      email: `user${i + 1}@example.com`,\n      score: Math.floor(Math.random() * 100),\n      category: ['A', 'B', 'C'][i % 3],\n      active: Math.random() > 0.3,\n      created_date: new Date(2024, Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1).toISOString().split('T')[0]\n    }));\n\n    const component = withDefaults({\n      id: 'large-dataframe',\n      type: 'dataframe',\n      data: {\n        data: largeData,\n        columns: ['id', 'name', 'email', 'score', 'category', 'active', 'created_date'],\n        title: 'Large Dataset',\n        description: '50 records with pagination and search',\n        row_count: largeData.length,\n        column_count: 7,\n        striped: args.striped ?? true,\n        bordered: args.bordered ?? true,\n        compact: args.compact ?? false,\n        searchable: args.searchable ?? true,\n        sortable: args.sortable ?? true,\n        exportable: args.exportable ?? true,\n        max_rows_displayed: 15,\n        column_types: {\n          id: 'number',\n          name: 'string',\n          email: 'string',\n          score: 'number',\n          category: 'string',\n          active: 'boolean',\n          created_date: 'date'\n        }\n      },\n    });\n\n    renderComponent(manager, component);\n    return container;\n  },\n  args: {\n    striped: true,\n    bordered: true,\n    compact: false,\n    searchable: true,\n    sortable: true,\n    exportable: true,\n  },\n};\n\nexport const DataTypesShowcase: Story = {\n  render: () => {\n    const container = createContainer();\n    const manager = createManager(container);\n\n    const typesData = [\n      {\n        id: 1,\n        name: 'Alice',\n        score: 95.5,\n        active: true,\n        created: '2024-01-15',\n        notes: 'Excellent performance',\n        tags: null\n      },\n      {\n        id: 2,\n        name: 'Bob',\n        score: 87.2,\n        active: false,\n        created: '2024-02-20',\n        notes: 'Good but needs improvement',\n        tags: 'priority,review'\n      },\n      {\n        id: 3,\n        name: 'Carol',\n        score: 92.8,\n        active: true,\n        created: '2024-03-10',\n        notes: null,\n        tags: 'star-performer'\n      },\n    ];\n\n    const component = withDefaults({\n      id: 'types-dataframe',\n      type: 'dataframe',\n      data: {\n        data: typesData,\n        columns: ['id', 'name', 'score', 'active', 'created', 'notes', 'tags'],\n        title: 'Data Types Showcase',\n        description: 'Demonstrates different column data types and null handling',\n        row_count: typesData.length,\n        column_count: 7,\n        striped: true,\n        bordered: true,\n        searchable: true,\n        sortable: true,\n        exportable: true,\n        column_types: {\n          id: 'number',\n          name: 'string',\n          score: 'number',\n          active: 'boolean',\n          created: 'date',\n          notes: 'string',\n          tags: 'string'\n        }\n      },\n    });\n\n    renderComponent(manager, component);\n    return container;\n  },\n};"
  },
  {
    "path": "frontends/webcomponent/src/components/plotly-chart.stories.ts",
    "content": "import type { Meta, StoryObj } from '@storybook/web-components';\nimport { html } from 'lit';\nimport './plotly-chart';\n\nconst meta: Meta = {\n  title: 'Rich Components/Plotly Chart',\n  component: 'plotly-chart',\n  parameters: {\n    layout: 'padded',\n    backgrounds: {\n      default: 'light',\n      values: [\n        { name: 'dark', value: 'rgb(11, 15, 25)' },\n        { name: 'light', value: '#f5f7fa' },\n      ],\n    },\n  },\n  argTypes: {\n    theme: {\n      control: 'select',\n      options: ['light', 'dark']\n    },\n    loading: { control: 'boolean' },\n    error: { control: 'text' },\n  },\n};\n\nexport default meta;\ntype Story = StoryObj;\n\nexport const LineChart: Story = {\n  args: {\n    theme: 'light',\n    loading: false,\n    error: '',\n  },\n  render: (args) => html`\n    <div style=\"max-width: 800px; margin: 0 auto;\">\n      <plotly-chart\n        theme=${args.theme}\n        ?loading=${args.loading}\n        error=${args.error}\n        .data=${[\n          {\n            x: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],\n            y: [20, 14, 23, 25, 22, 16],\n            type: 'scatter',\n            mode: 'lines+markers',\n            name: 'Sales',\n            line: { color: 'rgb(0, 123, 255)' }\n          }\n        ]}\n        .layout=${{\n          xaxis: { title: 'Month' },\n          yaxis: { title: 'Sales (in thousands)' }\n        }}>\n      </plotly-chart>\n    </div>\n  `,\n};\n\nexport const BarChart: Story = {\n  args: {\n    theme: 'light',\n  },\n  render: (args) => html`\n    <div style=\"max-width: 800px; margin: 0 auto;\">\n      <plotly-chart\n        theme=${args.theme}\n        .data=${[\n          {\n            x: ['Product A', 'Product B', 'Product C', 'Product D'],\n            y: [45, 32, 28, 35],\n            type: 'bar',\n            name: 'Revenue',\n            marker: {\n              color: ['rgb(16, 185, 129)', 'rgb(0, 123, 255)', 'rgb(245, 158, 11)', 'rgb(239, 68, 68)']\n            }\n          }\n        ]}\n        .layout=${{\n          xaxis: { title: 'Products' },\n          yaxis: { title: 'Revenue ($M)' }\n        }}>\n      </plotly-chart>\n    </div>\n  `,\n};\n\nexport const ScatterPlot: Story = {\n  args: {\n    theme: 'light',\n  },\n  render: (args) => html`\n    <div style=\"max-width: 800px; margin: 0 auto;\">\n      <plotly-chart\n        theme=${args.theme}\n        .data=${[\n          {\n            x: [85, 78, 92, 88, 76, 95, 82, 89, 93, 79],\n            y: [450, 320, 580, 490, 280, 650, 380, 520, 610, 310],\n            type: 'scatter',\n            mode: 'markers',\n            name: 'Business Units',\n            marker: {\n              size: 12,\n              color: 'rgb(0, 123, 255)',\n              opacity: 0.7\n            }\n          }\n        ]}\n        .layout=${{\n          xaxis: { title: 'Customer Satisfaction Score' },\n          yaxis: { title: 'Revenue ($K)' }\n        }}>\n      </plotly-chart>\n    </div>\n  `,\n};\n\nexport const MultipleLines: Story = {\n  args: {\n    theme: 'light',\n  },\n  render: (args) => html`\n    <div style=\"max-width: 800px; margin: 0 auto;\">\n      <plotly-chart\n        theme=${args.theme}\n        .data=${[\n          {\n            x: ['Q1', 'Q2', 'Q3', 'Q4'],\n            y: [85, 88, 92, 89],\n            type: 'scatter',\n            mode: 'lines+markers',\n            name: 'User Engagement',\n            line: { color: 'rgb(16, 185, 129)' }\n          },\n          {\n            x: ['Q1', 'Q2', 'Q3', 'Q4'],\n            y: [65, 72, 78, 81],\n            type: 'scatter',\n            mode: 'lines+markers',\n            name: 'Conversion Rate',\n            line: { color: 'rgb(0, 123, 255)' }\n          },\n          {\n            x: ['Q1', 'Q2', 'Q3', 'Q4'],\n            y: [42, 48, 55, 58],\n            type: 'scatter',\n            mode: 'lines+markers',\n            name: 'Customer Retention',\n            line: { color: 'rgb(245, 158, 11)' }\n          }\n        ]}\n        .layout=${{\n          xaxis: { title: 'Quarter' },\n          yaxis: { title: 'Percentage (%)' }\n        }}>\n      </plotly-chart>\n    </div>\n  `,\n};\n\nexport const LoadingState: Story = {\n  args: {\n    theme: 'light',\n    loading: true,\n  },\n  render: (args) => html`\n    <div style=\"max-width: 800px; margin: 0 auto;\">\n      <plotly-chart\n        theme=${args.theme}\n        ?loading=${args.loading}\n        .data=${[]}>\n      </plotly-chart>\n    </div>\n  `,\n};\n\nexport const ErrorState: Story = {\n  args: {\n    theme: 'light',\n    error: 'Failed to load chart data from API',\n  },\n  render: (args) => html`\n    <div style=\"max-width: 800px; margin: 0 auto;\">\n      <plotly-chart\n        theme=${args.theme}\n        error=${args.error}\n        .data=${[]}>\n      </plotly-chart>\n    </div>\n  `,\n};\n\nexport const LightTheme: Story = {\n  args: {\n    theme: 'light',\n  },\n  parameters: {\n    backgrounds: { default: 'light' }\n  },\n  render: (args) => html`\n    <div style=\"max-width: 800px; margin: 0 auto;\">\n      <plotly-chart\n        theme=${args.theme}\n        .data=${[\n          {\n            x: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],\n            y: [20, 14, 23, 25, 22, 16],\n            type: 'scatter',\n            mode: 'lines+markers',\n            name: 'Sales',\n            line: { color: 'rgb(0, 123, 255)' }\n          }\n        ]}\n        .layout=${{\n          xaxis: { title: 'Month' },\n          yaxis: { title: 'Sales (in thousands)' }\n        }}>\n      </plotly-chart>\n    </div>\n  `,\n};\n\nexport const CustomLayout: Story = {\n  args: {\n    theme: 'light',\n  },\n  render: (args) => html`\n    <div style=\"max-width: 800px; margin: 0 auto;\">\n      <plotly-chart\n        theme=${args.theme}\n        .data=${[\n          {\n            x: [1, 2, 3, 4, 5],\n            y: [10, 11, 12, 13, 14],\n            type: 'scatter',\n            mode: 'lines',\n            name: 'Trend A',\n            line: { color: 'rgb(16, 185, 129)', width: 3 }\n          },\n          {\n            x: [1, 2, 3, 4, 5],\n            y: [8, 9, 10, 11, 12],\n            type: 'scatter',\n            mode: 'lines',\n            name: 'Trend B',\n            line: { color: 'rgb(239, 68, 68)', width: 3, dash: 'dash' }\n          }\n        ]}\n        .layout=${{\n          title: {\n            text: 'Custom Styled Chart',\n            font: { size: 18 }\n          },\n          xaxis: {\n            title: 'Time Period',\n            gridcolor: 'rgba(255, 255, 255, 0.1)'\n          },\n          yaxis: {\n            title: 'Value',\n            gridcolor: 'rgba(255, 255, 255, 0.1)'\n          },\n          height: 500,\n          showlegend: true\n        }}>\n      </plotly-chart>\n    </div>\n  `,\n};\n\n"
  },
  {
    "path": "frontends/webcomponent/src/components/plotly-chart.ts",
    "content": "import { LitElement, html, css } from 'lit';\nimport { customElement, property } from 'lit/decorators.js';\nimport { vannaDesignTokens } from '../styles/vanna-design-tokens.js';\nimport Plotly from 'plotly.js-dist-min';\n\nexport interface PlotlyData {\n  x?: any[];\n  y?: any[];\n  type?: any;\n  mode?: any;\n  name?: string;\n  marker?: any;\n  line?: any;\n  [key: string]: any;\n}\n\nexport interface PlotlyLayout {\n  title?: any;\n  xaxis?: any;\n  yaxis?: any;\n  font?: any;\n  paper_bgcolor?: string;\n  plot_bgcolor?: string;\n  margin?: any;\n  showlegend?: boolean;\n  height?: number;\n  width?: number;\n  modebar?: any;\n  [key: string]: any;\n}\n\n@customElement('plotly-chart')\nexport class PlotlyChart extends LitElement {\n  static styles = [\n    vannaDesignTokens,\n    css`\n      :host {\n        display: block;\n        font-family: var(--vanna-font-family-default);\n        width: 100%;\n        height: 100%;\n      }\n\n      .plotly-div {\n        width: 100%;\n        min-height: 400px;\n      }\n\n      /* Plotly layering fix for Shadow DOM */\n      .plotly-div,\n      .plotly-div .js-plotly-plot,\n      .plotly-div .plot-container,\n      .plotly-div .svg-container {\n        position: relative;\n        width: 100%;\n        height: 100%;\n      }\n\n      .plotly-div svg.main-svg {\n        position: absolute;\n        top: 0;\n        left: 0;\n      }\n\n      .plotly-div .hoverlayer {\n        pointer-events: none;\n      }\n\n      .error-message {\n        padding: var(--vanna-space-4);\n        color: var(--vanna-accent-negative-default);\n        text-align: center;\n        font-style: italic;\n      }\n\n      .loading-message {\n        padding: var(--vanna-space-4);\n        color: var(--vanna-foreground-dimmer);\n        text-align: center;\n        font-style: italic;\n      }\n    `\n  ];\n\n  @property({ type: Array }) data: PlotlyData[] = [];\n  @property({ type: Object }) layout: PlotlyLayout = {};\n  @property({ type: Object }) config = {};\n  @property({ type: Boolean }) loading = false;\n  @property() error = '';\n  @property() theme: 'light' | 'dark' = 'dark';\n\n  private plotlyDiv?: HTMLElement;\n  private resizeObserver?: ResizeObserver;\n\n  firstUpdated() {\n    this.plotlyDiv = this.shadowRoot?.querySelector('.plotly-div') as HTMLElement;\n    this._renderChart();\n    this._setupResizeObserver();\n  }\n\n  disconnectedCallback() {\n    super.disconnectedCallback();\n    this.resizeObserver?.disconnect();\n  }\n\n  private _setupResizeObserver() {\n    if (!this.plotlyDiv) return;\n\n    this.resizeObserver = new ResizeObserver(() => {\n      if (this.plotlyDiv && this.data.length > 0) {\n        const width = this.plotlyDiv.offsetWidth;\n        Plotly.relayout(this.plotlyDiv, { width });\n      }\n    });\n\n    this.resizeObserver.observe(this.plotlyDiv);\n  }\n\n  updated(changedProperties: Map<string | number | symbol, unknown>) {\n    if (changedProperties.has('data') || changedProperties.has('layout') || changedProperties.has('theme')) {\n      this._renderChart();\n    }\n  }\n\n  private _getDefaultLayout(): PlotlyLayout {\n    const isDark = this.theme === 'dark';\n\n    // Start with layout from backend (which may include white background)\n    const mergedLayout = {\n      ...this.layout,\n      // Only add font/modebar if not already set by backend\n      font: this.layout.font || {\n        family: 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif',\n        color: isDark ? 'rgb(242, 244, 247)' : 'rgb(17, 24, 39)',\n        size: 12\n      },\n      modebar: this.layout.modebar || {\n        bgcolor: isDark ? 'rgba(21, 26, 38, 0.8)' : 'rgba(255, 255, 255, 0.8)',\n        color: isDark ? 'rgb(177, 186, 196)' : 'rgb(75, 85, 99)',\n        activecolor: isDark ? 'rgb(242, 244, 247)' : 'rgb(17, 24, 39)',\n        orientation: 'h'\n      },\n      // Set explicit dimensions for Shadow DOM compatibility\n      autosize: false,\n      width: this.layout.width || undefined,\n      height: this.layout.height || 400,\n    };\n\n    // If backend didn't set background colors, use transparent\n    if (!this.layout.paper_bgcolor) {\n      mergedLayout.paper_bgcolor = 'transparent';\n    }\n    if (!this.layout.plot_bgcolor) {\n      mergedLayout.plot_bgcolor = 'transparent';\n    }\n\n    return mergedLayout;\n  }\n\n  private _getDefaultConfig() {\n    return {\n      responsive: true,\n      displayModeBar: false,\n      ...this.config\n    };\n  }\n\n  private async _renderChart() {\n    if (!this.plotlyDiv || this.loading || this.error || this.data.length === 0) {\n      return;\n    }\n\n    try {\n      const layout = this._getDefaultLayout();\n      const config = this._getDefaultConfig();\n\n      await Plotly.newPlot(this.plotlyDiv, this.data, layout, config);\n    } catch (err) {\n      this.error = err instanceof Error ? err.message : 'Failed to render chart';\n      console.error('Plotly chart error:', err);\n    }\n  }\n\n  render() {\n    return html`\n      ${this.loading ? html`\n        <div class=\"loading-message\">Loading chart...</div>\n      ` : this.error ? html`\n        <div class=\"error-message\">Error: ${this.error}</div>\n      ` : html`\n        <div class=\"plotly-div\"></div>\n      `}\n    `;\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'plotly-chart': PlotlyChart;\n  }\n}"
  },
  {
    "path": "frontends/webcomponent/src/components/rich-card.stories.ts",
    "content": "import type { Meta, StoryObj } from '@storybook/web-components';\nimport { html } from 'lit';\nimport './rich-card';\n\nconst meta: Meta = {\n  title: 'Rich Components/Rich Card',\n  component: 'rich-card',\n  parameters: {\n    layout: 'padded',\n    backgrounds: {\n      default: 'light',\n      values: [\n        { name: 'dark', value: 'rgb(11, 15, 25)' },\n        { name: 'light', value: '#f5f7fa' },\n      ],\n    },\n  },\n  argTypes: {\n    title: { control: 'text' },\n    subtitle: { control: 'text' },\n    content: { control: 'text' },\n    icon: { control: 'text' },\n    status: {\n      control: 'select',\n      options: ['info', 'success', 'warning', 'error']\n    },\n    collapsible: { control: 'boolean' },\n    collapsed: { control: 'boolean' },\n  },\n};\n\nexport default meta;\ntype Story = StoryObj;\n\nexport const Default: Story = {\n  args: {\n    title: 'Sample Card',\n    subtitle: 'This is a subtitle',\n    content: 'This is the content of the card. It can contain any text or HTML.',\n    status: 'info',\n    collapsible: false,\n    collapsed: false,\n  },\n  render: (args) => html`\n    <div style=\"max-width: 600px; margin: 0 auto;\">\n      <rich-card\n        title=${args.title}\n        subtitle=${args.subtitle}\n        content=${args.content}\n        icon=${args.icon}\n        status=${args.status}\n        ?collapsible=${args.collapsible}\n        ?collapsed=${args.collapsed}>\n      </rich-card>\n    </div>\n  `,\n};\n\nexport const WithIcon: Story = {\n  args: {\n    title: 'Card with Icon',\n    subtitle: 'Featuring an emoji icon',\n    content: 'This card demonstrates how icons work with the rich card component.',\n    icon: '🚀',\n    status: 'success',\n    collapsible: false,\n  },\n  render: (args) => html`\n    <div style=\"max-width: 600px; margin: 0 auto;\">\n      <rich-card\n        title=${args.title}\n        subtitle=${args.subtitle}\n        content=${args.content}\n        icon=${args.icon}\n        status=${args.status}\n        ?collapsible=${args.collapsible}>\n      </rich-card>\n    </div>\n  `,\n};\n\nexport const WithActions: Story = {\n  args: {\n    title: 'Interactive Card',\n    subtitle: 'With action buttons',\n    content: 'This card includes action buttons that can trigger events.',\n    status: 'info',\n    collapsible: false,\n  },\n  render: (args) => html`\n    <div style=\"max-width: 600px; margin: 0 auto;\">\n      <rich-card\n        title=${args.title}\n        subtitle=${args.subtitle}\n        content=${args.content}\n        status=${args.status}\n        ?collapsible=${args.collapsible}\n        .actions=${[\n          { label: 'Primary Action', action: 'primary', variant: 'primary' },\n          { label: 'Secondary Action', action: 'secondary', variant: 'secondary' }\n        ]}\n        @card-action=${(e: CustomEvent) => {\n          console.log('Card action:', e.detail.action);\n          alert(`Action triggered: ${e.detail.action}`);\n        }}>\n      </rich-card>\n    </div>\n  `,\n};\n\nexport const Collapsible: Story = {\n  args: {\n    title: 'Collapsible Card',\n    subtitle: 'Click to expand/collapse',\n    content: 'This content can be hidden by clicking the toggle button in the header.',\n    status: 'warning',\n    collapsible: true,\n    collapsed: false,\n  },\n  render: (args) => html`\n    <div style=\"max-width: 600px; margin: 0 auto;\">\n      <rich-card\n        title=${args.title}\n        subtitle=${args.subtitle}\n        content=${args.content}\n        status=${args.status}\n        ?collapsible=${args.collapsible}\n        ?collapsed=${args.collapsed}>\n      </rich-card>\n    </div>\n  `,\n};\n\nexport const StatusVariants: Story = {\n  render: () => html`\n    <div style=\"max-width: 600px; margin: 0 auto; display: flex; flex-direction: column; gap: 16px;\">\n      <rich-card\n        title=\"Info Status\"\n        content=\"This card shows the info status variant.\"\n        status=\"info\">\n      </rich-card>\n\n      <rich-card\n        title=\"Success Status\"\n        content=\"This card shows the success status variant.\"\n        status=\"success\">\n      </rich-card>\n\n      <rich-card\n        title=\"Warning Status\"\n        content=\"This card shows the warning status variant.\"\n        status=\"warning\">\n      </rich-card>\n\n      <rich-card\n        title=\"Error Status\"\n        content=\"This card shows the error status variant.\"\n        status=\"error\">\n      </rich-card>\n    </div>\n  `,\n};\n\nexport const LightTheme: Story = {\n  args: {\n    title: 'Light Theme Card',\n    subtitle: 'Styled for light backgrounds',\n    content: 'This card is displayed with the light theme variant.',\n    icon: '☀️',\n    status: 'success',\n  },\n  parameters: {\n    backgrounds: { default: 'light' }\n  },\n  render: (args) => html`\n    <div style=\"max-width: 600px; margin: 0 auto;\">\n      <rich-card\n        theme=\"light\"\n        title=${args.title}\n        subtitle=${args.subtitle}\n        content=${args.content}\n        icon=${args.icon}\n        status=${args.status}>\n      </rich-card>\n    </div>\n  `,\n};"
  },
  {
    "path": "frontends/webcomponent/src/components/rich-card.ts",
    "content": "import { LitElement, html, css } from 'lit';\nimport { customElement, property } from 'lit/decorators.js';\nimport { vannaDesignTokens } from '../styles/vanna-design-tokens.js';\n\nexport interface CardAction {\n  label: string;\n  action: string;\n  variant?: 'primary' | 'secondary';\n}\n\n@customElement('rich-card')\nexport class RichCard extends LitElement {\n  static styles = [\n    vannaDesignTokens,\n    css`\n      :host {\n        display: block;\n        margin-bottom: var(--vanna-space-4);\n        font-family: var(--vanna-font-family-default);\n      }\n\n      .card {\n        border: 1px solid var(--vanna-outline-default);\n        border-radius: var(--vanna-border-radius-lg);\n        background: var(--vanna-background-default);\n        box-shadow: var(--vanna-shadow-sm);\n        overflow: hidden;\n        transition: box-shadow var(--vanna-duration-200) ease;\n      }\n\n      .card:hover {\n        box-shadow: var(--vanna-shadow-md);\n      }\n\n      .card-header {\n        display: flex;\n        align-items: center;\n        padding: var(--vanna-space-4) var(--vanna-space-5);\n        background: var(--vanna-background-higher);\n        border-bottom: 1px solid var(--vanna-outline-default);\n        gap: var(--vanna-space-3);\n      }\n\n      .card-header.collapsible {\n        cursor: pointer;\n      }\n\n      .card-icon {\n        font-size: 1.25rem;\n        display: flex;\n        align-items: center;\n      }\n\n      .card-title-section {\n        flex: 1;\n      }\n\n      .card-title {\n        margin: 0;\n        font-size: 1rem;\n        font-weight: 600;\n        color: var(--vanna-foreground-default);\n      }\n\n      .card-subtitle {\n        margin: var(--vanna-space-1) 0 0 0;\n        font-size: 0.875rem;\n        color: var(--vanna-foreground-dimmer);\n      }\n\n      .card-status {\n        padding: var(--vanna-space-1) var(--vanna-space-2);\n        border-radius: var(--vanna-border-radius-md);\n        font-size: 0.75rem;\n        font-weight: 600;\n        text-transform: uppercase;\n      }\n\n      .card-status.status-success {\n        background: #d4edda;\n        color: #155724;\n      }\n\n      .card-status.status-warning {\n        background: #fff3cd;\n        color: #856404;\n      }\n\n      .card-status.status-error {\n        background: #f8d7da;\n        color: #721c24;\n      }\n\n      .card-status.status-info {\n        background: #d1ecf1;\n        color: #0c5460;\n      }\n\n      .card-toggle {\n        background: none;\n        border: none;\n        cursor: pointer;\n        font-size: 1rem;\n        color: var(--vanna-foreground-dimmer);\n        padding: var(--vanna-space-1);\n        border-radius: var(--vanna-border-radius-sm);\n        transition: background-color var(--vanna-duration-200) ease;\n      }\n\n      .card-toggle:hover {\n        background: var(--vanna-background-root);\n      }\n\n      .card-content {\n        padding: var(--vanna-space-4) var(--vanna-space-5);\n        line-height: 1.5;\n        color: var(--vanna-foreground-default);\n        transition: all var(--vanna-duration-200) ease;\n        overflow: hidden;\n      }\n\n      .card-content.collapsed {\n        max-height: 0;\n        padding-top: 0;\n        padding-bottom: 0;\n      }\n\n      .card-content h1,\n      .card-content h2,\n      .card-content h3 {\n        margin: var(--vanna-space-2) 0;\n        font-weight: 600;\n      }\n\n      .card-content h1 {\n        font-size: 1.5rem;\n      }\n\n      .card-content h2 {\n        font-size: 1.25rem;\n      }\n\n      .card-content h3 {\n        font-size: 1.125rem;\n      }\n\n      .card-content p {\n        margin: var(--vanna-space-2) 0;\n      }\n\n      .card-content ul {\n        margin: var(--vanna-space-2) 0;\n        padding-left: var(--vanna-space-5);\n      }\n\n      .card-content li {\n        margin: var(--vanna-space-1) 0;\n      }\n\n      .card-content code {\n        background: var(--vanna-background-higher);\n        padding: var(--vanna-space-1) var(--vanna-space-2);\n        border-radius: var(--vanna-border-radius-sm);\n        font-family: monospace;\n        font-size: 0.875em;\n      }\n\n      .card-content strong {\n        font-weight: 600;\n      }\n\n      .card-actions {\n        padding: var(--vanna-space-3) var(--vanna-space-5);\n        background: var(--vanna-background-root);\n        border-top: 1px solid var(--vanna-outline-default);\n        display: flex;\n        gap: var(--vanna-space-2);\n      }\n\n      .card-action {\n        padding: var(--vanna-space-2) var(--vanna-space-4);\n        border-radius: var(--vanna-border-radius-md);\n        border: 1px solid var(--vanna-outline-default);\n        background: var(--vanna-background-default);\n        color: var(--vanna-foreground-default);\n        cursor: pointer;\n        font-size: 0.875rem;\n        font-weight: 500;\n        transition: all var(--vanna-duration-200) ease;\n      }\n\n      .card-action:hover {\n        background: var(--vanna-background-higher);\n      }\n\n      .card-action.primary {\n        background: var(--vanna-accent-primary-default);\n        color: white;\n        border-color: var(--vanna-accent-primary-default);\n      }\n\n      .card-action.primary:hover {\n        background: var(--vanna-accent-primary-stronger);\n      }\n    `\n  ];\n\n  @property() title = '';\n  @property() subtitle = '';\n  @property() content = '';\n  @property() icon = '';\n  @property() status: 'info' | 'success' | 'warning' | 'error' = 'info';\n  @property({ type: Array }) actions: CardAction[] = [];\n  @property({ type: Boolean }) collapsible = false;\n  @property({ type: Boolean }) collapsed = false;\n  @property({ type: Boolean }) markdown = false;\n  @property() theme: 'light' | 'dark' = 'dark';\n\n  private _toggleCollapsed() {\n    if (this.collapsible) {\n      this.collapsed = !this.collapsed;\n    }\n  }\n\n  private _renderMarkdown(text: string): string {\n    // Simple markdown rendering - basic formatting\n    return text\n      .replace(/^### (.*$)/gm, '<h3>$1</h3>')\n      .replace(/^## (.*$)/gm, '<h2>$1</h2>')\n      .replace(/^# (.*$)/gm, '<h1>$1</h1>')\n      .replace(/\\*\\*(.*?)\\*\\*/g, '<strong>$1</strong>')\n      .replace(/\\*(.*?)\\*/g, '<em>$1</em>')\n      .replace(/`([^`]+)`/g, '<code>$1</code>')\n      .replace(/^- (.*$)/gm, '<li>$1</li>')\n      .replace(/(<li>.*<\\/li>)/s, '<ul>$1</ul>')\n      .replace(/\\n\\n/g, '</p><p>')\n      .replace(/^(?!<[h|u|l|p])(.+)$/gm, '<p>$1</p>');\n  }\n\n  render() {\n    const contentHtml = this.markdown\n      ? html`<div class=\"card-content ${this.collapsed ? 'collapsed' : ''}\" .innerHTML=${this._renderMarkdown(this.content)}></div>`\n      : html`<div class=\"card-content ${this.collapsed ? 'collapsed' : ''}\">${this.content}</div>`;\n\n    return html`\n      <div class=\"card\">\n        <div class=\"card-header ${this.collapsible ? 'collapsible' : ''}\"\n             @click=${this._toggleCollapsed}>\n          ${this.icon ? html`<span class=\"card-icon\">${this.icon}</span>` : ''}\n          <div class=\"card-title-section\">\n            <h3 class=\"card-title\">${this.title}</h3>\n            ${this.subtitle ? html`<p class=\"card-subtitle\">${this.subtitle}</p>` : ''}\n          </div>\n          ${this.status ? html`<span class=\"card-status status-${this.status}\">${this.status}</span>` : ''}\n          ${this.collapsible ? html`\n            <button class=\"card-toggle\">${this.collapsed ? '▶' : '▼'}</button>\n          ` : ''}\n        </div>\n        ${contentHtml}\n        ${this.actions.length > 0 ? html`\n          <div class=\"card-actions\">\n            ${this.actions.map(action => html`\n              <button class=\"card-action ${action.variant || 'secondary'}\"\n                      @click=${() => this._handleAction(action.action)}>\n                ${action.label}\n              </button>\n            `)}\n          </div>\n        ` : ''}\n      </div>\n    `;\n  }\n\n  private async _handleAction(action: string) {\n    console.log('🔘 Card action button clicked (rich-card)');\n    console.log('   Action:', action);\n\n    // Dispatch event for any listeners\n    this.dispatchEvent(new CustomEvent('card-action', {\n      detail: { action },\n      bubbles: true,\n      composed: true\n    }));\n\n    // Also directly send to vanna-chat\n    const vannaChat = document.querySelector('vanna-chat') as any;\n    if (vannaChat && typeof vannaChat.sendMessage === 'function') {\n      console.log('   Found vanna-chat, sending message...');\n      try {\n        const success = await vannaChat.sendMessage(action);\n        if (success) {\n          console.log('   ✅ Action sent successfully');\n        } else {\n          console.error('   ❌ Failed to send action');\n        }\n      } catch (error) {\n        console.error('   ❌ Error sending action:', error);\n      }\n    } else {\n      console.warn('   ⚠️ vanna-chat component not found or sendMessage not available');\n    }\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'rich-card': RichCard;\n  }\n}"
  },
  {
    "path": "frontends/webcomponent/src/components/rich-component-system.stories.ts",
    "content": "import type { Meta, StoryObj } from '@storybook/web-components';\nimport { ComponentManager, ComponentUpdate } from './rich-component-system';\nimport { vannaDesignTokens } from '../styles/vanna-design-tokens.js';\n\nconst meta: Meta = {\n  title: 'Rich Components/Component System',\n  parameters: {\n    layout: 'padded',\n    backgrounds: {\n      default: 'light',\n      values: [\n        { name: 'light', value: '#f5f7fa' },\n        { name: 'dark', value: 'rgb(11, 15, 25)' },\n      ],\n    },\n  },\n};\n\nexport default meta;\ntype Story = StoryObj;\n\nconst ensureTokenStyles = () => {\n  if (document.getElementById('vanna-token-style')) {\n    return;\n  }\n\n  const style = document.createElement('style');\n  style.id = 'vanna-token-style';\n  style.textContent = vannaDesignTokens.cssText.replace(/:host/g, '.vanna-tokens');\n  document.head.appendChild(style);\n};\n\nconst createContainer = () => {\n  ensureTokenStyles();\n\n  const container = document.createElement('div');\n  container.className = 'vanna-tokens';\n  container.style.cssText = `\n    padding: var(--vanna-space-5, 20px);\n    max-width: 800px;\n    margin: 0 auto;\n    background: var(--vanna-background-default);\n    border-radius: var(--vanna-border-radius-lg);\n    box-shadow: var(--vanna-shadow-md);\n  `;\n\n  return container;\n};\n\nconst createManager = (container: HTMLElement) => new ComponentManager(container);\n\nconst renderComponent = (manager: ComponentManager, component: any) => {\n  const update: ComponentUpdate = {\n    operation: 'create',\n    target_id: component.id,\n    component,\n    timestamp: new Date().toISOString(),\n  } as ComponentUpdate;\n\n  manager.processUpdate(update);\n};\n\nconst withDefaults = (component: any) => ({\n  layout: { position: 'append', size: {}, z_index: 0, classes: [] },\n  theme: {},\n  lifecycle: 'create',\n  ...component,\n});\n\nexport const NotificationComponents: Story = {\n  render: () => {\n    const container = createContainer();\n    const manager = createManager(container);\n\n    const components = [\n      withDefaults({\n        id: 'info-notification',\n        type: 'notification',\n        data: {\n          message: 'This is an informational message',\n          title: 'Information',\n          level: 'info',\n          dismissible: true,\n          actions: [],\n        },\n      }),\n      withDefaults({\n        id: 'success-notification',\n        type: 'notification',\n        data: {\n          message: 'Operation completed successfully!',\n          title: 'Success',\n          level: 'success',\n          dismissible: true,\n          actions: [\n            { label: 'View Details', action: 'view', variant: 'primary' },\n            { label: 'Dismiss', action: 'dismiss', variant: 'secondary' },\n          ],\n        },\n      }),\n      withDefaults({\n        id: 'warning-notification',\n        type: 'notification',\n        data: {\n          message: 'Please review the configuration before proceeding',\n          title: 'Warning',\n          level: 'warning',\n          dismissible: true,\n          actions: [],\n        },\n      }),\n      withDefaults({\n        id: 'error-notification',\n        type: 'notification',\n        data: {\n          message: 'Failed to connect to the database. Please check your connection.',\n          title: 'Connection Error',\n          level: 'error',\n          dismissible: true,\n          actions: [\n            { label: 'Retry', action: 'retry', variant: 'primary' },\n            { label: 'Cancel', action: 'cancel', variant: 'secondary' },\n          ],\n        },\n      }),\n    ];\n\n    components.forEach((component) => renderComponent(manager, component));\n\n    return container;\n  },\n};\n\nexport const StatusIndicatorComponents: Story = {\n  render: () => {\n    const container = createContainer();\n    const manager = createManager(container);\n\n    const statuses = [\n      { status: 'loading', message: 'Processing your request...', pulse: true },\n      { status: 'success', message: 'Request completed successfully', pulse: false },\n      { status: 'warning', message: 'Operation completed with warnings', pulse: false },\n      { status: 'error', message: 'Request failed - please try again', pulse: false },\n    ];\n\n    statuses.forEach((statusData, index) => {\n      const component = withDefaults({\n        id: `status-${index}`,\n        type: 'status_indicator',\n        data: statusData,\n      });\n\n      renderComponent(manager, component);\n    });\n\n    return container;\n  },\n};\n\nexport const TextComponents: Story = {\n  render: () => {\n    const container = createContainer();\n    const manager = createManager(container);\n\n    const plainText = withDefaults({\n      id: 'plain-text',\n      type: 'text',\n      data: {\n        content: 'This is a plain text component with some sample content to demonstrate text rendering.',\n        markdown: false,\n      },\n    });\n\n    const markdownText = withDefaults({\n      id: 'markdown-text',\n      type: 'text',\n      data: {\n        content: `# Rich Components Demo\\n\\nThis is a **markdown** text component with various formatting:\\n\\n- **Bold text** for emphasis\\n- *Italic text* for style\\n- Lists for organization\\n\\n## Features\\n\\nThe text component supports:\\n- Plain text rendering\\n- Basic markdown formatting\\n- Code syntax highlighting`,\n        markdown: true,\n      },\n    });\n\n    [plainText, markdownText].forEach((component) => renderComponent(manager, component));\n\n    return container;\n  },\n};\n\nexport const DataFrameComponents: Story = {\n  render: () => {\n    const container = createContainer();\n    const manager = createManager(container);\n\n    // Sample data for different scenarios\n    const sampleData = [\n      { id: 1, name: 'Alice Johnson', email: 'alice@example.com', age: 28, city: 'New York', salary: 75000, active: true },\n      { id: 2, name: 'Bob Smith', email: 'bob@example.com', age: 34, city: 'San Francisco', salary: 85000, active: true },\n      { id: 3, name: 'Carol Davis', email: 'carol@example.com', age: 29, city: 'Chicago', salary: 70000, active: false },\n      { id: 4, name: 'David Wilson', email: 'david@example.com', age: 42, city: 'Austin', salary: 90000, active: true },\n      { id: 5, name: 'Eve Brown', email: 'eve@example.com', age: 31, city: 'Seattle', salary: 80000, active: true },\n      { id: 6, name: 'Frank Miller', email: 'frank@example.com', age: 38, city: 'Boston', salary: 95000, active: false },\n      { id: 7, name: 'Grace Lee', email: 'grace@example.com', age: 26, city: 'Denver', salary: 65000, active: true },\n      { id: 8, name: 'Henry Taylor', email: 'henry@example.com', age: 33, city: 'Portland', salary: 72000, active: true },\n    ];\n\n    const columns = ['id', 'name', 'email', 'age', 'city', 'salary', 'active'];\n\n    // Basic DataFrame\n    const basicDataFrame = withDefaults({\n      id: 'basic-dataframe',\n      type: 'dataframe',\n      data: {\n        data: sampleData.slice(0, 5),\n        columns: columns,\n        title: 'Employee Records',\n        description: 'Sample employee data with various fields',\n        row_count: 5,\n        column_count: columns.length,\n        column_types: {\n          id: 'number',\n          name: 'string',\n          email: 'string',\n          age: 'number',\n          city: 'string',\n          salary: 'number',\n          active: 'boolean'\n        }\n      },\n    });\n\n    // Large DataFrame with all features\n    const fullDataFrame = withDefaults({\n      id: 'full-dataframe',\n      type: 'dataframe',\n      data: {\n        data: sampleData,\n        columns: columns,\n        title: 'Complete Employee Database',\n        description: 'Full dataset with search, sort, and export functionality',\n        row_count: sampleData.length,\n        column_count: columns.length,\n        searchable: true,\n        sortable: true,\n        filterable: true,\n        exportable: true,\n        striped: true,\n        bordered: true,\n        max_rows_displayed: 6,\n        column_types: {\n          id: 'number',\n          name: 'string',\n          email: 'string',\n          age: 'number',\n          city: 'string',\n          salary: 'number',\n          active: 'boolean'\n        }\n      },\n    });\n\n    // Empty DataFrame\n    const emptyDataFrame = withDefaults({\n      id: 'empty-dataframe',\n      type: 'dataframe',\n      data: {\n        data: [],\n        columns: [],\n        title: 'Empty Dataset',\n        description: 'No data available to display',\n        row_count: 0,\n        column_count: 0,\n      },\n    });\n\n    // Compact DataFrame\n    const compactDataFrame = withDefaults({\n      id: 'compact-dataframe',\n      type: 'dataframe',\n      data: {\n        data: sampleData.slice(0, 4),\n        columns: ['id', 'name', 'city', 'active'],\n        title: 'Compact View',\n        description: 'Space-efficient display with essential columns only',\n        row_count: 4,\n        column_count: 4,\n        compact: true,\n        searchable: false,\n        exportable: false,\n        column_types: {\n          id: 'number',\n          name: 'string',\n          city: 'string',\n          active: 'boolean'\n        }\n      },\n    });\n\n    [basicDataFrame, fullDataFrame, emptyDataFrame, compactDataFrame].forEach((component) => {\n      renderComponent(manager, component);\n    });\n\n    return container;\n  },\n};\n\nexport const SQLQueryDataFrame: Story = {\n  render: () => {\n    const container = createContainer();\n    const manager = createManager(container);\n\n    // SQL query result simulation\n    const sqlResultData = [\n      { TrackId: 1, Name: 'For Those About To Rock (We Salute You)', AlbumId: 1, MediaTypeId: 1, GenreId: 1, Composer: 'Angus Young, Malcolm Young, Brian Johnson', Milliseconds: 343719, Bytes: 11170334, UnitPrice: 0.99 },\n      { TrackId: 2, Name: 'Balls to the Wall', AlbumId: 2, MediaTypeId: 2, GenreId: 1, Composer: null, Milliseconds: 342562, Bytes: 5510424, UnitPrice: 0.99 },\n      { TrackId: 3, Name: 'Fast As a Shark', AlbumId: 3, MediaTypeId: 2, GenreId: 1, Composer: 'F. Baltes, S. Kaufman, U. Dirkscneider & W. Hoffman', Milliseconds: 230619, Bytes: 3990994, UnitPrice: 0.99 },\n      { TrackId: 4, Name: 'Restless and Wild', AlbumId: 3, MediaTypeId: 2, GenreId: 1, Composer: 'F. Baltes, R.A. Smith-Diesel, S. Kaufman, U. Dirkscneider & W. Hoffman', Milliseconds: 252051, Bytes: 4331779, UnitPrice: 0.99 },\n      { TrackId: 5, Name: 'Princess of the Dawn', AlbumId: 3, MediaTypeId: 2, GenreId: 1, Composer: 'Deaffy & R.A. Smith-Diesel', Milliseconds: 375418, Bytes: 6290521, UnitPrice: 0.99 },\n    ];\n\n    const sqlColumns = ['TrackId', 'Name', 'AlbumId', 'MediaTypeId', 'GenreId', 'Composer', 'Milliseconds', 'Bytes', 'UnitPrice'];\n\n    const sqlDataFrame = withDefaults({\n      id: 'sql-dataframe',\n      type: 'dataframe',\n      data: {\n        data: sqlResultData,\n        columns: sqlColumns,\n        title: 'Query Results',\n        description: 'SELECT * FROM Track LIMIT 5',\n        row_count: sqlResultData.length,\n        column_count: sqlColumns.length,\n        searchable: true,\n        sortable: true,\n        exportable: true,\n        column_types: {\n          TrackId: 'number',\n          Name: 'string',\n          AlbumId: 'number',\n          MediaTypeId: 'number',\n          GenreId: 'number',\n          Composer: 'string',\n          Milliseconds: 'number',\n          Bytes: 'number',\n          UnitPrice: 'number'\n        }\n      },\n    });\n\n    renderComponent(manager, sqlDataFrame);\n\n    return container;\n  },\n};\n"
  },
  {
    "path": "frontends/webcomponent/src/components/rich-component-system.ts",
    "content": "/**\n * Rich Component System for Vanna Agents\n *\n * Provides a generic component registry and rendering system that can display\n * any rich component sent from the Python backend.\n */\n\nimport { richComponentStyleText } from '../styles/rich-component-styles.js';\n\n// Component interfaces matching Python backend\nexport interface RichComponent {\n  id: string;\n  type: string;\n  lifecycle: 'create' | 'update' | 'replace' | 'remove';\n  data: Record<string, any>;\n  children: string[];\n  timestamp: string;\n  visible: boolean;\n  interactive: boolean;\n}\n\n// Artifact event interfaces\nexport interface ArtifactOpenedEventDetail {\n  // Core identification\n  artifactId: string;\n\n  // Artifact content\n  content: string; // Full HTML/SVG/JS content\n  type: 'html' | 'svg' | 'visualization' | 'interactive' | 'd3' | 'threejs';\n  title?: string;\n  description?: string;\n\n  // Trigger context\n  trigger: 'created' | 'user-action'; // How this event was fired\n\n  // Control\n  preventDefault: () => void; // Prevent default behavior\n\n  // Helpers\n  getStandaloneHTML: () => string; // Full page HTML with dependencies\n\n  // Metadata\n  timestamp: string;\n}\n\ndeclare global {\n  interface GlobalEventHandlersEventMap {\n    'artifact-opened': CustomEvent<ArtifactOpenedEventDetail>;\n  }\n}\n\n\nconst RICH_COMPONENT_STYLE_ATTR = 'data-vanna-rich-component-styles';\n\nfunction ensureRichComponentStyles(container: HTMLElement): void {\n  const doc = container.ownerDocument;\n  if (!doc) {\n    return;\n  }\n\n  if (container.querySelector(`style[${RICH_COMPONENT_STYLE_ATTR}]`)) {\n    return;\n  }\n\n  const styleEl = doc.createElement('style');\n  styleEl.setAttribute(RICH_COMPONENT_STYLE_ATTR, 'true');\n  styleEl.textContent = richComponentStyleText;\n  container.prepend(styleEl);\n}\n\nexport interface ComponentUpdate {\n  operation: 'create' | 'update' | 'replace' | 'remove' | 'reorder' | 'bulk_update';\n  target_id: string;\n  component?: RichComponent;\n  updates?: Record<string, any>;\n  position?: any;\n  timestamp: string;\n  batch_id?: string;\n}\n\n// Component renderer interface\nexport interface ComponentRenderer {\n  render(component: RichComponent): HTMLElement;\n  update(element: HTMLElement, component: RichComponent, updates?: Record<string, any>): void;\n  remove(element: HTMLElement): void;\n}\n\n// Base component renderer with common functionality\nexport abstract class BaseComponentRenderer implements ComponentRenderer {\n  abstract render(component: RichComponent): HTMLElement;\n\n  update(element: HTMLElement, component: RichComponent, _updates?: Record<string, any>): void {\n    // Default implementation - re-render completely\n    const newElement = this.render(component);\n    element.parentNode?.replaceChild(newElement, element);\n  }\n\n  remove(element: HTMLElement): void {\n    element.remove();\n  }\n\n}\n\n// Card component renderer\nexport class CardComponentRenderer extends BaseComponentRenderer {\n  render(component: RichComponent): HTMLElement {\n    console.log('🎴 CardComponentRenderer.render() called', {\n      componentId: component.id,\n      componentData: component.data,\n      actions: component.data?.actions\n    });\n\n    const card = document.createElement('div');\n    card.className = 'rich-component rich-card';\n    card.dataset.componentId = component.id;\n\n    const { title, content, subtitle, icon, status, actions = [], collapsible, collapsed } = component.data;\n\n    console.log('🎴 Extracted actions:', actions, 'Length:', actions?.length);\n\n    card.innerHTML = `\n      <div class=\"card-header ${collapsible ? 'collapsible' : ''}\">\n        ${icon ? `<span class=\"card-icon\">${icon}</span>` : ''}\n        <div class=\"card-title-section\">\n          <h3 class=\"card-title\">${title}</h3>\n          ${subtitle ? `<p class=\"card-subtitle\">${subtitle}</p>` : ''}\n        </div>\n        ${status ? `<span class=\"card-status status-${status}\">${status}</span>` : ''}\n        ${collapsible ? `<button class=\"card-toggle\">${collapsed ? '▶' : '▼'}</button>` : ''}\n      </div>\n      <div class=\"card-content ${collapsed ? 'collapsed' : ''}\">\n        ${content}\n      </div>\n      ${actions && actions.length > 0 ? `\n        <div class=\"card-actions\">\n          ${actions.map((action: any) => `\n            <button class=\"card-action ${action.variant || 'secondary'}\" data-action=\"${action.action}\">\n              ${action.label}\n            </button>\n          `).join('')}\n        </div>\n      ` : ''}\n    `;\n\n\n    // Add collapsible functionality\n    if (collapsible) {\n      const toggle = card.querySelector('.card-toggle') as HTMLButtonElement;\n      const content = card.querySelector('.card-content') as HTMLElement;\n\n      toggle?.addEventListener('click', () => {\n        content.classList.toggle('collapsed');\n        toggle.textContent = content.classList.contains('collapsed') ? '▶' : '▼';\n      });\n    }\n\n    // Add click handlers for action buttons\n    console.log('🎴 Checking if should add click handlers:', {\n      hasActions: !!actions,\n      actionsLength: actions?.length\n    });\n\n    if (actions && actions.length > 0) {\n      const actionButtons = card.querySelectorAll('.card-action') as NodeListOf<HTMLButtonElement>;\n      console.log('🎴 Found action buttons:', actionButtons.length);\n\n      actionButtons.forEach((button, index) => {\n        const action = actions[index];\n        console.log(`🎴 Setting up listener for button ${index}:`, {\n          hasAction: !!action,\n          hasActionProperty: !!action?.action,\n          action: action\n        });\n\n        if (action && action.action) {\n          console.log('🎴 Adding click listener to button:', button);\n          button.addEventListener('click', async () => {\n            console.log('🔘 Card action button clicked:', action.label);\n            console.log('   Sending action:', action.action);\n\n            // Apply visual feedback\n            button.disabled = true;\n            button.classList.add('button-transitioning', 'button-clicked');\n\n            // Find vanna-chat component and send message\n            const vannaChat = document.querySelector('vanna-chat') as any;\n\n            if (vannaChat && typeof vannaChat.sendMessage === 'function') {\n              try {\n                const success = await vannaChat.sendMessage(action.action);\n\n                if (success) {\n                  console.log('✅ Card action sent successfully');\n                  // Keep button disabled after successful action\n                } else {\n                  console.error('❌ Failed to send card action');\n                  // Re-enable button on failure\n                  button.disabled = false;\n                  button.classList.remove('button-transitioning', 'button-clicked');\n                }\n              } catch (error) {\n                console.error('❌ Error sending card action:', error);\n                // Re-enable button on error\n                button.disabled = false;\n                button.classList.remove('button-transitioning', 'button-clicked');\n              }\n            } else {\n              console.warn('⚠️ vanna-chat component not found or sendMessage not available');\n              button.disabled = false;\n              button.classList.remove('button-transitioning', 'button-clicked');\n            }\n          });\n        }\n      });\n    }\n\n    return card;\n  }\n\n  update(element: HTMLElement, component: RichComponent, updates?: Record<string, any>): void {\n    if (!updates) return super.update(element, component);\n\n    // Optimized updates for common properties\n    if (updates.title) {\n      const titleEl = element.querySelector('.card-title');\n      if (titleEl) titleEl.textContent = updates.title;\n    }\n\n    if (updates.content) {\n      const contentEl = element.querySelector('.card-content');\n      if (contentEl) contentEl.innerHTML = updates.content;\n    }\n\n    if (updates.status) {\n      const statusEl = element.querySelector('.card-status');\n      if (statusEl) {\n        statusEl.className = `card-status status-${updates.status}`;\n        statusEl.textContent = updates.status;\n      }\n    }\n\n    // For complex updates, fall back to full re-render\n    if (updates.actions || updates.collapsible) {\n      super.update(element, component);\n    }\n  }\n}\n\n// Task list component renderer\nexport class TaskListComponentRenderer extends BaseComponentRenderer {\n  render(component: RichComponent): HTMLElement {\n    const container = document.createElement('div');\n    container.className = 'rich-component rich-task-list';\n    container.dataset.componentId = component.id;\n\n    const { title, tasks = [], show_progress, show_timestamps } = component.data;\n\n    const completedTasks = tasks.filter((task: any) => task.status === 'completed').length;\n    const progress = tasks.length > 0 ? (completedTasks / tasks.length) * 100 : 0;\n\n    container.innerHTML = `\n      <div class=\"task-list-header\">\n        <h3 class=\"task-list-title\">${title}</h3>\n        ${show_progress ? `\n          <div class=\"task-list-progress\">\n            <span class=\"progress-text\">${completedTasks}/${tasks.length} completed</span>\n            <div class=\"progress-bar\">\n              <div class=\"progress-fill\" style=\"width: ${progress}%\"></div>\n            </div>\n          </div>\n        ` : ''}\n      </div>\n      <div class=\"task-list-items\">\n        ${tasks.map((task: any) => this.renderTask(task, show_timestamps)).join('')}\n      </div>\n    `;\n\n\n    return container;\n  }\n\n  private renderTask(task: any, showTimestamps: boolean): string {\n    const statusIcon = this.getStatusIcon(task.status);\n    const progressBar = task.progress !== null && task.progress !== undefined ? `\n      <div class=\"task-progress\">\n        <div class=\"task-progress-bar\">\n          <div class=\"task-progress-fill\" style=\"width: ${task.progress * 100}%\"></div>\n        </div>\n        <span class=\"task-progress-text\">${Math.round(task.progress * 100)}%</span>\n      </div>\n    ` : '';\n\n    return `\n      <div class=\"task-item status-${task.status}\" data-task-id=\"${task.id}\">\n        <div class=\"task-icon\">${statusIcon}</div>\n        <div class=\"task-content\">\n          <div class=\"task-title\">${task.title}</div>\n          ${task.description ? `<div class=\"task-description\">${task.description}</div>` : ''}\n          ${progressBar}\n          ${showTimestamps && task.created_at ? `\n            <div class=\"task-timestamp\">\n              Created: ${new Date(task.created_at).toLocaleString()}\n            </div>\n          ` : ''}\n        </div>\n      </div>\n    `;\n  }\n\n  private getStatusIcon(status: string): string {\n    switch (status) {\n      case 'completed': return '✅';\n      case 'running': return '🔄';\n      case 'failed': return '❌';\n      default: return '⭕';\n    }\n  }\n}\n\n// Progress bar component renderer\nexport class ProgressBarComponentRenderer extends BaseComponentRenderer {\n  render(component: RichComponent): HTMLElement {\n    const container = document.createElement('div');\n    container.className = 'rich-component rich-progress-bar';\n    container.dataset.componentId = component.id;\n\n    const { value, label, show_percentage, status, animated } = component.data;\n    const percentage = Math.round(value * 100);\n\n    container.innerHTML = `\n      <div class=\"progress-header\">\n        ${label ? `<span class=\"progress-label\">${label}</span>` : ''}\n        ${show_percentage ? `<span class=\"progress-percentage\">${percentage}%</span>` : ''}\n      </div>\n      <div class=\"progress-track\">\n        <div class=\"progress-fill ${animated ? 'animated' : ''} ${status ? `status-${status}` : ''}\"\n             style=\"width: ${percentage}%\"></div>\n      </div>\n    `;\n\n\n    return container;\n  }\n\n  update(element: HTMLElement, component: RichComponent, updates?: Record<string, any>): void {\n    if (!updates) return super.update(element, component);\n\n    if (updates.value !== undefined) {\n      const fill = element.querySelector('.progress-fill') as HTMLElement;\n      const percentage = Math.round(updates.value * 100);\n\n      if (fill) {\n        fill.style.width = `${percentage}%`;\n      }\n\n      const percentageEl = element.querySelector('.progress-percentage');\n      if (percentageEl) {\n        percentageEl.textContent = `${percentage}%`;\n      }\n    }\n\n    if (updates.label) {\n      const labelEl = element.querySelector('.progress-label');\n      if (labelEl) labelEl.textContent = updates.label;\n    }\n\n    if (updates.status) {\n      const fill = element.querySelector('.progress-fill') as HTMLElement;\n      if (fill) {\n        fill.className = fill.className.replace(/status-\\w+/, `status-${updates.status}`);\n      }\n    }\n  }\n}\n\n// Notification component renderer\nexport class NotificationComponentRenderer extends BaseComponentRenderer {\n  render(component: RichComponent): HTMLElement {\n    const container = document.createElement('div');\n    container.className = 'rich-component rich-notification';\n    container.dataset.componentId = component.id;\n\n    const { message, title, level = 'info', icon, dismissible, auto_dismiss, actions = [] } = component.data;\n\n    const levelIcon = icon || this.getLevelIcon(level);\n    const dismissButton = dismissible ? `\n      <button class=\"notification-dismiss\" onclick=\"this.parentElement.remove()\">×</button>\n    ` : '';\n\n    container.innerHTML = `\n      <div class=\"notification-content level-${level}\">\n        ${levelIcon ? `<span class=\"notification-icon\">${levelIcon}</span>` : ''}\n        <div class=\"notification-body\">\n          ${title ? `<div class=\"notification-title\">${title}</div>` : ''}\n          <div class=\"notification-message\">${message}</div>\n        </div>\n        ${actions.length > 0 ? `\n          <div class=\"notification-actions\">\n            ${actions.map((action: any) => `\n              <button class=\"notification-action ${action.variant || 'secondary'}\" data-action=\"${action.action}\">\n                ${action.label}\n              </button>\n            `).join('')}\n          </div>\n        ` : ''}\n        ${dismissButton}\n      </div>\n    `;\n\n    // Auto-dismiss functionality\n    if (auto_dismiss && component.data.auto_dismiss_delay) {\n      setTimeout(() => {\n        if (container.parentElement) {\n          container.remove();\n        }\n      }, component.data.auto_dismiss_delay);\n    }\n\n\n    return container;\n  }\n\n  private getLevelIcon(level: string): string {\n    switch (level) {\n      case 'success': return '✅';\n      case 'warning': return '⚠️';\n      case 'error': return '❌';\n      case 'info':\n      default: return 'ℹ️';\n    }\n  }\n}\n\n// Status indicator component renderer\nexport class StatusIndicatorComponentRenderer extends BaseComponentRenderer {\n  render(component: RichComponent): HTMLElement {\n    const container = document.createElement('div');\n    container.className = 'rich-component rich-status-indicator';\n    container.dataset.componentId = component.id;\n\n    const { status, message, icon, pulse } = component.data;\n\n    const statusIcon = icon || this.getStatusIcon(status);\n    const pulseClass = pulse ? 'pulse' : '';\n\n    container.innerHTML = `\n      <div class=\"status-indicator-content status-${status} ${pulseClass}\">\n        <span class=\"status-icon\">${statusIcon}</span>\n        <span class=\"status-message\">${message}</span>\n      </div>\n    `;\n\n\n    return container;\n  }\n\n  private getStatusIcon(status: string): string {\n    switch (status) {\n      case 'loading': return '🔄';\n      case 'success': return '✅';\n      case 'warning': return '⚠️';\n      case 'error': return '❌';\n      default: return 'ℹ️';\n    }\n  }\n\n  update(element: HTMLElement, component: RichComponent, updates?: Record<string, any>): void {\n    if (!updates) return super.update(element, component);\n\n    const content = element.querySelector('.status-indicator-content');\n    if (content && updates.status) {\n      content.className = content.className.replace(/status-\\w+/, `status-${updates.status}`);\n    }\n\n    if (updates.pulse !== undefined) {\n      const content = element.querySelector('.status-indicator-content');\n      if (content) {\n        if (updates.pulse) {\n          content.classList.add('pulse');\n        } else {\n          content.classList.remove('pulse');\n        }\n      }\n    }\n\n    if (updates.message) {\n      const messageEl = element.querySelector('.status-message');\n      if (messageEl) {\n        messageEl.textContent = updates.message;\n      }\n    }\n  }\n}\n\n// DataFrame component renderer\nexport class DataFrameComponentRenderer extends BaseComponentRenderer {\n  render(component: RichComponent): HTMLElement {\n    const container = document.createElement('div');\n    container.className = 'rich-component rich-dataframe';\n    container.dataset.componentId = component.id;\n\n    const {\n      data = [],\n      columns = [],\n      title,\n      description,\n      row_count = 0,\n      column_count = 0,\n      max_rows_displayed = 100,\n      searchable = true,\n      sortable = true,\n      filterable = true,\n      exportable = true,\n      striped = true,\n      bordered = true,\n      compact = false,\n      column_types = {}\n    } = component.data;\n\n    // Limit displayed rows\n    const displayedData = data.slice(0, max_rows_displayed);\n    const hasMoreRows = data.length > max_rows_displayed;\n\n    let headerHTML = '';\n    if (title || description) {\n      headerHTML = `\n        <div class=\"dataframe-header\">\n          ${title ? `<h3 class=\"dataframe-title\">${title}</h3>` : ''}\n          ${description ? `<p class=\"dataframe-description\">${description}</p>` : ''}\n          <div class=\"dataframe-meta\">\n            <span class=\"row-count\">${row_count} rows</span>\n            <span class=\"column-count\">${column_count} columns</span>\n          </div>\n        </div>\n      `;\n    }\n\n    let actionsHTML = '';\n    if (searchable || exportable || filterable) {\n      actionsHTML = `\n        <div class=\"dataframe-actions\">\n          ${searchable ? `\n            <div class=\"dataframe-search\">\n              <input type=\"text\" placeholder=\"Search...\" class=\"search-input\">\n            </div>\n          ` : ''}\n          ${exportable ? `\n            <button class=\"export-btn\" title=\"Export to CSV\">📥 Export</button>\n          ` : ''}\n        </div>\n      `;\n    }\n\n    let tableHTML = '';\n    if (columns.length > 0 && displayedData.length > 0) {\n      const tableClasses = [\n        'dataframe-table',\n        striped ? 'striped' : '',\n        bordered ? 'bordered' : '',\n        compact ? 'compact' : ''\n      ].filter(Boolean).join(' ');\n\n      tableHTML = `\n        <div class=\"dataframe-table-container\">\n          <table class=\"${tableClasses}\">\n            <thead>\n              <tr>\n                ${columns.map((col: string) => `\n                  <th class=\"${sortable ? 'sortable' : ''}\" data-column=\"${col}\">\n                    ${col}\n                    ${sortable ? '<span class=\"sort-indicator\"></span>' : ''}\n                  </th>\n                `).join('')}\n              </tr>\n            </thead>\n            <tbody>\n              ${displayedData.map((row: any) => `\n                <tr>\n                  ${columns.map((col: string) => {\n                    const value = row[col];\n                    const columnType = column_types[col] || 'string';\n                    const formattedValue = this.formatCellValue(value, columnType);\n                    return `<td class=\"cell-${columnType}\">${formattedValue}</td>`;\n                  }).join('')}\n                </tr>\n              `).join('')}\n            </tbody>\n          </table>\n          ${hasMoreRows ? `\n            <div class=\"dataframe-truncated\">\n              <em>Showing ${max_rows_displayed} of ${row_count} rows</em>\n            </div>\n          ` : ''}\n        </div>\n      `;\n    } else {\n      tableHTML = `\n        <div class=\"dataframe-empty\">\n          <p>No data to display</p>\n        </div>\n      `;\n    }\n\n    container.innerHTML = `\n      ${headerHTML}\n      ${actionsHTML}\n      ${tableHTML}\n    `;\n\n\n    // Add event listeners\n    this.attachEventListeners(container, displayedData, columns);\n\n    return container;\n  }\n\n  private formatCellValue(value: any, columnType: string): string {\n    if (value === null || value === undefined) {\n      return '<em class=\"null-value\">NULL</em>';\n    }\n\n    switch (columnType) {\n      case 'number':\n        return typeof value === 'number' ? value.toLocaleString() : String(value);\n      case 'date':\n        try {\n          return new Date(value).toLocaleDateString();\n        } catch {\n          return String(value);\n        }\n      case 'boolean':\n        return value ? '✓' : '✗';\n      default:\n        return this.escapeHtml(String(value));\n    }\n  }\n\n  private escapeHtml(text: string): string {\n    const div = document.createElement('div');\n    div.textContent = text;\n    return div.innerHTML;\n  }\n\n  private attachEventListeners(container: HTMLElement, data: any[], columns: string[]): void {\n    // Search functionality\n    const searchInput = container.querySelector('.search-input') as HTMLInputElement;\n    if (searchInput) {\n      searchInput.addEventListener('input', (e) => {\n        const searchTerm = (e.target as HTMLInputElement).value.toLowerCase();\n        this.filterTable(container, data, columns, searchTerm);\n      });\n    }\n\n    // Export functionality\n    const exportBtn = container.querySelector('.export-btn') as HTMLButtonElement;\n    if (exportBtn) {\n      exportBtn.addEventListener('click', () => {\n        this.exportToCSV(data, columns);\n      });\n    }\n\n    // Sort functionality\n    const sortableHeaders = container.querySelectorAll('th.sortable');\n    sortableHeaders.forEach(header => {\n      header.addEventListener('click', (e) => {\n        const column = (e.currentTarget as HTMLElement).dataset.column;\n        if (column) {\n          this.sortTable(container, data, columns, column);\n        }\n      });\n    });\n  }\n\n  private filterTable(container: HTMLElement, data: any[], columns: string[], searchTerm: string): void {\n    const tbody = container.querySelector('tbody');\n    if (!tbody) return;\n\n    const filteredData = data.filter(row => {\n      return columns.some(col => {\n        const value = String(row[col] || '').toLowerCase();\n        return value.includes(searchTerm);\n      });\n    });\n\n    tbody.innerHTML = filteredData.map(row => `\n      <tr>\n        ${columns.map(col => {\n          const value = row[col];\n          const formattedValue = this.formatCellValue(value, 'string');\n          return `<td>${formattedValue}</td>`;\n        }).join('')}\n      </tr>\n    `).join('');\n  }\n\n  private sortTable(container: HTMLElement, data: any[], columns: string[], column: string): void {\n    const tbody = container.querySelector('tbody');\n    const header = container.querySelector(`th[data-column=\"${column}\"]`) as HTMLElement;\n    if (!tbody || !header) return;\n\n    // Determine sort direction\n    const currentSort = header.dataset.sortDirection || 'none';\n    const newSort = currentSort === 'asc' ? 'desc' : 'asc';\n\n    // Clear all sort indicators\n    container.querySelectorAll('th[data-sort-direction]').forEach(h => {\n      (h as HTMLElement).removeAttribute('data-sort-direction');\n      const indicator = h.querySelector('.sort-indicator');\n      if (indicator) indicator.textContent = '';\n    });\n\n    // Set new sort direction\n    header.dataset.sortDirection = newSort;\n    const indicator = header.querySelector('.sort-indicator');\n    if (indicator) {\n      indicator.textContent = newSort === 'asc' ? '↑' : '↓';\n    }\n\n    // Sort data\n    const sortedData = [...data].sort((a, b) => {\n      const aVal = a[column];\n      const bVal = b[column];\n\n      if (aVal === null || aVal === undefined) return 1;\n      if (bVal === null || bVal === undefined) return -1;\n\n      if (typeof aVal === 'number' && typeof bVal === 'number') {\n        return newSort === 'asc' ? aVal - bVal : bVal - aVal;\n      }\n\n      const aStr = String(aVal).toLowerCase();\n      const bStr = String(bVal).toLowerCase();\n      const comparison = aStr.localeCompare(bStr);\n      return newSort === 'asc' ? comparison : -comparison;\n    });\n\n    // Update table\n    tbody.innerHTML = sortedData.map(row => `\n      <tr>\n        ${columns.map(col => {\n          const value = row[col];\n          const formattedValue = this.formatCellValue(value, 'string');\n          return `<td>${formattedValue}</td>`;\n        }).join('')}\n      </tr>\n    `).join('');\n  }\n\n  private exportToCSV(data: any[], columns: string[]): void {\n    const csvContent = [\n      columns.join(','),\n      ...data.map(row =>\n        columns.map(col => {\n          const value = row[col];\n          const strValue = value === null || value === undefined ? '' : String(value);\n          // Escape quotes and wrap in quotes if contains comma, quote, or newline\n          if (strValue.includes(',') || strValue.includes('\"') || strValue.includes('\\n')) {\n            return `\"${strValue.replace(/\"/g, '\"\"')}\"`;\n          }\n          return strValue;\n        }).join(',')\n      )\n    ].join('\\n');\n\n    const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });\n    const link = document.createElement('a');\n    const url = URL.createObjectURL(blob);\n    link.setAttribute('href', url);\n    link.setAttribute('download', 'data.csv');\n    link.style.visibility = 'hidden';\n    document.body.appendChild(link);\n    link.click();\n    document.body.removeChild(link);\n  }\n}\n\n// Text component renderer\nexport class TextComponentRenderer extends BaseComponentRenderer {\n  render(component: RichComponent): HTMLElement {\n    const container = document.createElement('div');\n    container.className = 'rich-component rich-text';\n    container.dataset.componentId = component.id;\n\n    const {\n      content,\n      markdown = false,\n      code_language,\n      font_size,\n      font_weight,\n      text_align\n    } = component.data;\n\n    // Apply text styling\n    let textStyle = '';\n    if (font_size) textStyle += `font-size: ${font_size}; `;\n    if (font_weight) textStyle += `font-weight: ${font_weight}; `;\n    if (text_align) textStyle += `text-align: ${text_align}; `;\n\n    if (code_language) {\n      // Code block\n      container.innerHTML = `\n        <pre class=\"text-code\" style=\"${textStyle}\"><code class=\"language-${code_language}\">${this.escapeHtml(content)}</code></pre>\n      `;\n    } else if (markdown) {\n      // Markdown text (simple implementation)\n      container.innerHTML = `\n        <div class=\"text-markdown\" style=\"${textStyle}\">${this.renderMarkdown(content)}</div>\n      `;\n    } else {\n      // Plain text\n      container.innerHTML = `\n        <div class=\"text-content\" style=\"${textStyle}\">${this.escapeHtml(content)}</div>\n      `;\n    }\n\n\n    return container;\n  }\n\n  private escapeHtml(text: string): string {\n    const div = document.createElement('div');\n    div.textContent = text;\n    return div.innerHTML;\n  }\n\n  private renderMarkdown(text: string): string {\n    // Simple markdown rendering - just basic formatting\n    return text\n      .replace(/^## (.*$)/gm, '<h2>$1</h2>')\n      .replace(/^# (.*$)/gm, '<h1>$1</h1>')\n      .replace(/\\*\\*(.*?)\\*\\*/g, '<strong>$1</strong>')\n      .replace(/\\*(.*?)\\*/g, '<em>$1</em>')\n      .replace(/^- (.*$)/gm, '<li>$1</li>')\n      .replace(/(<li>.*<\\/li>)/s, '<ul>$1</ul>')\n      .replace(/\\n\\n/g, '</p><p>')\n      .replace(/^(?!<[h|u|l])(.+)$/gm, '<p>$1</p>');\n  }\n}\n\n// Primitive Component Renderers (Domain-Agnostic)\n\n// Status card component renderer\nexport class StatusCardComponentRenderer extends BaseComponentRenderer {\n  render(component: RichComponent): HTMLElement {\n    const container = document.createElement('div');\n    container.className = 'rich-component rich-status-card';\n    container.dataset.componentId = component.id;\n\n    const { title, status, description, icon, actions = [], collapsible, collapsed, metadata = {} } = component.data;\n\n    const statusIcon = icon || this.getStatusIcon(status);\n    const hasMetadata = Object.keys(metadata).length > 0;\n\n    container.innerHTML = `\n      <div class=\"status-card-header ${collapsible ? 'collapsible' : ''}\">\n        <div class=\"status-card-icon\">${statusIcon}</div>\n        <div class=\"status-card-title-section\">\n          <h3 class=\"status-card-title\">${title}</h3>\n          <span class=\"status-card-badge status-${status}\">${status}</span>\n        </div>\n        ${collapsible ? `<button class=\"status-card-toggle\">${collapsed ? '▶' : '▼'}</button>` : ''}\n      </div>\n      ${description ? `\n        <div class=\"status-card-content ${collapsed ? 'collapsed' : ''}\">\n          ${description}\n        </div>\n      ` : ''}\n      ${hasMetadata ? `\n        <details class=\"status-card-metadata\">\n          <summary class=\"status-card-metadata-summary\">Parameters</summary>\n          <div class=\"status-card-metadata-content\">\n            ${this.renderMetadataTable(metadata)}\n          </div>\n        </details>\n      ` : ''}\n      ${actions.length > 0 ? `\n        <div class=\"status-card-actions\">\n          ${actions.map((action: any) => `\n            <button class=\"status-card-action ${action.variant || 'secondary'}\" data-action=\"${action.action}\">\n              ${action.label}\n            </button>\n          `).join('')}\n        </div>\n      ` : ''}\n    `;\n\n    // Add collapsible functionality\n    if (collapsible) {\n      const toggle = container.querySelector('.status-card-toggle') as HTMLButtonElement;\n      const content = container.querySelector('.status-card-content') as HTMLElement;\n\n      toggle?.addEventListener('click', () => {\n        if (content) {\n          content.classList.toggle('collapsed');\n          toggle.textContent = content.classList.contains('collapsed') ? '▶' : '▼';\n        }\n      });\n    }\n\n    return container;\n  }\n\n  private renderMetadataTable(metadata: Record<string, any>): string {\n    const rows = Object.entries(metadata).map(([key, value]) => {\n      const formattedValue = this.formatMetadataValue(value);\n      return `\n        <tr>\n          <td class=\"metadata-key\">${this.escapeHtml(key)}</td>\n          <td class=\"metadata-value\">${formattedValue}</td>\n        </tr>\n      `;\n    }).join('');\n\n    return `\n      <table class=\"metadata-table\">\n        <thead>\n          <tr>\n            <th>Parameter</th>\n            <th>Value</th>\n          </tr>\n        </thead>\n        <tbody>\n          ${rows}\n        </tbody>\n      </table>\n    `;\n  }\n\n  private formatMetadataValue(value: any): string {\n    if (value === null) {\n      return '<span class=\"metadata-null\">null</span>';\n    }\n    if (value === undefined) {\n      return '<span class=\"metadata-undefined\">undefined</span>';\n    }\n    if (typeof value === 'boolean') {\n      return `<span class=\"metadata-boolean\">${value}</span>`;\n    }\n    if (typeof value === 'number') {\n      return `<span class=\"metadata-number\">${value}</span>`;\n    }\n    if (typeof value === 'string') {\n      return `<span class=\"metadata-string\">${this.escapeHtml(value)}</span>`;\n    }\n    if (Array.isArray(value) || typeof value === 'object') {\n      return `<pre class=\"metadata-json\">${JSON.stringify(value, null, 2)}</pre>`;\n    }\n    return this.escapeHtml(String(value));\n  }\n\n  private escapeHtml(text: string): string {\n    const div = document.createElement('div');\n    div.textContent = text;\n    return div.innerHTML;\n  }\n\n  private getStatusIcon(status: string): string {\n    switch (status) {\n      case 'pending': return '⏳';\n      case 'running': return '⚙️';\n      case 'completed': return '✅';\n      case 'success': return '✅';\n      case 'failed': return '❌';\n      case 'error': return '❌';\n      case 'warning': return '⚠️';\n      default: return 'ℹ️';\n    }\n  }\n}\n\n// Progress display component renderer\nexport class ProgressDisplayComponentRenderer extends BaseComponentRenderer {\n  render(component: RichComponent): HTMLElement {\n    const container = document.createElement('div');\n    container.className = 'rich-component rich-progress-display';\n    container.dataset.componentId = component.id;\n\n    const { label, value, description, status, show_percentage, animated, indeterminate } = component.data;\n    const percentage = Math.round(value * 100);\n\n    container.innerHTML = `\n      <div class=\"progress-display-container\">\n        <div class=\"progress-display-header\">\n          <span class=\"progress-display-label\">${label}</span>\n          ${show_percentage && !indeterminate ? `<span class=\"progress-display-percentage\">${percentage}%</span>` : ''}\n        </div>\n        <div class=\"progress-display-track\">\n          <div class=\"progress-display-fill ${animated ? 'animated' : ''} ${status ? `status-${status}` : ''} ${indeterminate ? 'indeterminate' : ''}\"\n               style=\"width: ${indeterminate ? '100' : percentage}%\"></div>\n        </div>\n        ${description ? `<div class=\"progress-display-description\">${description}</div>` : ''}\n      </div>\n    `;\n\n    return container;\n  }\n}\n\n// Log viewer component renderer\nexport class LogViewerComponentRenderer extends BaseComponentRenderer {\n  render(component: RichComponent): HTMLElement {\n    const container = document.createElement('div');\n    container.className = 'rich-component rich-log-viewer';\n    container.dataset.componentId = component.id;\n\n    const { title, entries = [], searchable, show_timestamps, auto_scroll } = component.data;\n\n    container.innerHTML = `\n      <div class=\"log-viewer-container\">\n        <div class=\"log-viewer-header\">\n          <h3 class=\"log-viewer-title\">${title}</h3>\n          ${searchable ? `\n            <div class=\"log-viewer-search\">\n              <input type=\"text\" placeholder=\"Search logs...\" class=\"log-search-input\">\n            </div>\n          ` : ''}\n        </div>\n        <div class=\"log-viewer-content ${auto_scroll ? 'auto-scroll' : ''}\">\n          ${entries.map((entry: any) => `\n            <div class=\"log-entry log-${entry.level}\">\n              ${show_timestamps ? `<span class=\"log-timestamp\">${new Date(entry.timestamp).toLocaleTimeString()}</span>` : ''}\n              <span class=\"log-level\">[${entry.level.toUpperCase()}]</span>\n              <span class=\"log-message\">${entry.message}</span>\n            </div>\n          `).join('')}\n        </div>\n      </div>\n    `;\n\n    // Auto-scroll to bottom if enabled\n    if (auto_scroll) {\n      const content = container.querySelector('.log-viewer-content');\n      if (content) {\n        content.scrollTop = content.scrollHeight;\n      }\n    }\n\n    return container;\n  }\n}\n\n// Badge component renderer\nexport class BadgeComponentRenderer extends BaseComponentRenderer {\n  render(component: RichComponent): HTMLElement {\n    const container = document.createElement('span');\n    container.className = `rich-component rich-badge badge-${component.data.variant} badge-${component.data.size}`;\n    container.dataset.componentId = component.id;\n\n    const { text, icon } = component.data;\n\n    container.innerHTML = `\n      ${icon ? `<span class=\"badge-icon\">${icon}</span>` : ''}\n      <span class=\"badge-text\">${text}</span>\n    `;\n\n    return container;\n  }\n}\n\n// Icon text component renderer\nexport class IconTextComponentRenderer extends BaseComponentRenderer {\n  render(component: RichComponent): HTMLElement {\n    const container = document.createElement('div');\n    container.className = `rich-component rich-icon-text icon-text-${component.data.variant} icon-text-${component.data.size} icon-text-${component.data.alignment}`;\n    container.dataset.componentId = component.id;\n\n    const { icon, text } = component.data;\n\n    container.innerHTML = `\n      <span class=\"icon-text-icon\">${icon}</span>\n      <span class=\"icon-text-text\">${text}</span>\n    `;\n\n    return container;\n  }\n}\n\n// Button component renderer\nexport class ButtonComponentRenderer extends BaseComponentRenderer {\n  render(component: RichComponent): HTMLElement {\n    const button = document.createElement('button');\n    button.className = `rich-component rich-button button-${component.data.variant} button-${component.data.size}`;\n    button.dataset.componentId = component.id;\n\n    const { label, action, disabled, icon, icon_position, full_width, loading } = component.data;\n\n    if (disabled || loading) {\n      button.disabled = true;\n    }\n\n    if (full_width) {\n      button.classList.add('button-full-width');\n    }\n\n    if (loading) {\n      button.classList.add('button-loading');\n    }\n\n    // Build button content\n    let buttonContent = '';\n    if (loading) {\n      buttonContent = `<span class=\"button-spinner\">⏳</span><span class=\"button-label\">${label}</span>`;\n    } else if (icon) {\n      if (icon_position === 'right') {\n        buttonContent = `<span class=\"button-label\">${label}</span><span class=\"button-icon\">${icon}</span>`;\n      } else {\n        buttonContent = `<span class=\"button-icon\">${icon}</span><span class=\"button-label\">${label}</span>`;\n      }\n    } else {\n      buttonContent = `<span class=\"button-label\">${label}</span>`;\n    }\n\n    button.innerHTML = buttonContent;\n\n    // Add click handler\n    if (action && !disabled && !loading) {\n      button.addEventListener('click', async () => {\n        console.log('🔘 Button clicked:', label);\n        console.log('   Sending action:', action);\n\n        // Apply visual feedback immediately\n        button.disabled = true;\n        button.classList.add('button-transitioning', 'button-clicked');\n\n        // Find vanna-chat component and send message with button action\n        const vannaChat = document.querySelector('vanna-chat') as any;\n        console.log('   Found vanna-chat:', !!vannaChat);\n\n        if (vannaChat && typeof vannaChat.sendMessage === 'function') {\n          console.log('   Calling sendMessage...');\n          try {\n            const success = await vannaChat.sendMessage(action);\n            if (success) {\n              console.log('   ✓ Message sent successfully');\n            } else {\n              console.log('   ✗ Message failed, restoring button state');\n              // Restore button state if it wasn't originally disabled\n              if (!disabled) {\n                button.disabled = false;\n              }\n              button.classList.remove('button-transitioning', 'button-clicked');\n            }\n          } catch (error) {\n            console.error('   ✗ Message failed with error:', error);\n            // Restore button state if it wasn't originally disabled\n            if (!disabled) {\n              button.disabled = false;\n            }\n            button.classList.remove('button-transitioning', 'button-clicked');\n          }\n        } else {\n          console.error('   ✗ vanna-chat not found or sendMessage not available');\n          // Restore button state if it wasn't originally disabled\n          if (!disabled) {\n            button.disabled = false;\n          }\n          button.classList.remove('button-transitioning', 'button-clicked');\n        }\n      });\n    }\n\n    return button;\n  }\n\n  update(element: HTMLElement, component: RichComponent, updates?: Record<string, any>): void {\n    if (!updates) return super.update(element, component);\n\n    const button = element as HTMLButtonElement;\n\n    if (updates.disabled !== undefined) {\n      button.disabled = updates.disabled;\n    }\n\n    if (updates.loading !== undefined) {\n      button.disabled = updates.loading;\n      if (updates.loading) {\n        button.classList.add('button-loading');\n      } else {\n        button.classList.remove('button-loading');\n      }\n    }\n\n    if (updates.label || updates.icon || updates.icon_position) {\n      // Re-render content\n      super.update(element, component);\n    }\n  }\n}\n\n// Button group component renderer\nexport class ButtonGroupComponentRenderer extends BaseComponentRenderer {\n  render(component: RichComponent): HTMLElement {\n    const container = document.createElement('div');\n    container.className = `rich-component rich-button-group button-group-${component.data.orientation} button-group-spacing-${component.data.spacing} button-group-align-${component.data.align}`;\n    container.dataset.componentId = component.id;\n\n    const { buttons = [], full_width } = component.data;\n\n    if (full_width) {\n      container.classList.add('button-group-full-width');\n    }\n\n    // Render each button\n    buttons.forEach((buttonConfig: any, index: number) => {\n      const button = document.createElement('button');\n      button.className = `rich-button button-${buttonConfig.variant || 'secondary'} button-${buttonConfig.size || 'medium'}`;\n      button.dataset.buttonIndex = String(index);\n\n      // Store original disabled state\n      if (buttonConfig.disabled) {\n        button.disabled = true;\n        button.dataset.originallyDisabled = 'true';\n      } else {\n        button.dataset.originallyDisabled = 'false';\n      }\n\n      // Build button content\n      let buttonContent = '';\n      if (buttonConfig.icon) {\n        if (buttonConfig.icon_position === 'right') {\n          buttonContent = `<span class=\"button-label\">${buttonConfig.label}</span><span class=\"button-icon\">${buttonConfig.icon}</span>`;\n        } else {\n          buttonContent = `<span class=\"button-icon\">${buttonConfig.icon}</span><span class=\"button-label\">${buttonConfig.label}</span>`;\n        }\n      } else {\n        buttonContent = `<span class=\"button-label\">${buttonConfig.label}</span>`;\n      }\n\n      button.innerHTML = buttonContent;\n\n      // Add click handler with enhanced functionality\n      if (buttonConfig.action && !buttonConfig.disabled) {\n        button.addEventListener('click', async () => {\n          console.log('🔘 Button Group button clicked:', buttonConfig.label);\n          console.log('   Button index:', index);\n          console.log('   Sending action:', buttonConfig.action);\n\n          // Immediately apply visual changes to all buttons in the group\n          this.applyButtonGroupClickState(container, index);\n\n          // Find vanna-chat component and send message with button action\n          const vannaChat = document.querySelector('vanna-chat') as any;\n          console.log('   Found vanna-chat:', !!vannaChat);\n\n          if (vannaChat && typeof vannaChat.sendMessage === 'function') {\n            console.log('   Calling sendMessage...');\n            try {\n              const success = await vannaChat.sendMessage(buttonConfig.action);\n              if (success) {\n                console.log('   ✓ Message sent successfully');\n              } else {\n                console.log('   ✗ Message failed, restoring button state');\n                this.restoreButtonGroupState(container);\n              }\n            } catch (error) {\n              console.error('   ✗ Message failed with error:', error);\n              this.restoreButtonGroupState(container);\n            }\n          } else {\n            console.error('   ✗ vanna-chat not found or sendMessage not available');\n            this.restoreButtonGroupState(container);\n          }\n        });\n      }\n\n      container.appendChild(button);\n    });\n\n    return container;\n  }\n\n  private applyButtonGroupClickState(container: HTMLElement, clickedIndex: number): void {\n    const buttons = container.querySelectorAll('button') as NodeListOf<HTMLButtonElement>;\n    \n    buttons.forEach((button, index) => {\n      // Disable all buttons\n      button.disabled = true;\n      \n      // Add transition class for animation\n      button.classList.add('button-transitioning');\n      \n      if (index === clickedIndex) {\n        // Highlight the clicked button\n        button.classList.add('button-clicked', 'button-highlighted');\n      } else {\n        // Gray out other buttons\n        button.classList.add('button-grayed-out');\n      }\n    });\n  }\n\n  private restoreButtonGroupState(container: HTMLElement): void {\n    const buttons = container.querySelectorAll('button') as NodeListOf<HTMLButtonElement>;\n    \n    buttons.forEach((button) => {\n      // Re-enable buttons (unless they were originally disabled)\n      const originallyDisabled = button.dataset.originallyDisabled === 'true';\n      if (!originallyDisabled) {\n        button.disabled = false;\n      }\n      \n      // Remove all state classes\n      button.classList.remove(\n        'button-clicked', \n        'button-highlighted', \n        'button-grayed-out',\n        'button-transitioning'\n      );\n    });\n  }\n}\n\n// Chart component renderer (for Plotly charts)\nexport class ChartComponentRenderer extends BaseComponentRenderer {\n  render(component: RichComponent): HTMLElement {\n    const container = document.createElement('div');\n    container.className = 'rich-component rich-chart';\n    container.dataset.componentId = component.id;\n\n    // The ChartComponent.data field contains the Plotly figure directly\n    // Structure: component.data = { data: [...traces...], layout: {...}, title: \"...\", config: {...} }\n    const { data: plotlyData, layout, title, config = {} } = component.data;\n\n    console.log('ChartComponentRenderer: Received component.data:', component.data);\n    console.log('ChartComponentRenderer: plotlyData:', plotlyData);\n    console.log('ChartComponentRenderer: layout:', layout);\n\n    // Check if we have a valid Plotly figure structure\n    if (plotlyData && Array.isArray(plotlyData) && layout) {\n      // Create plotly-chart web component\n      const chartElement = document.createElement('plotly-chart') as any;\n\n      // Set theme to match current theme\n      const vannaChat = document.querySelector('vanna-chat');\n      if (vannaChat) {\n        chartElement.theme = vannaChat.getAttribute('theme') || 'dark';\n      }\n\n      // Wrap in container with optional title\n      if (title) {\n        container.innerHTML = `\n          <div class=\"chart-header\">\n            <h3 class=\"chart-title\">${title}</h3>\n          </div>\n          <div class=\"chart-content\"></div>\n        `;\n        container.querySelector('.chart-content')?.appendChild(chartElement);\n      } else {\n        container.appendChild(chartElement);\n      }\n\n      // Set data AFTER the element is in the DOM\n      // This ensures the web component is fully initialized\n      requestAnimationFrame(() => {\n        chartElement.data = plotlyData; // Plotly traces (array)\n        chartElement.layout = layout; // Plotly layout (object)\n        chartElement.config = config;\n\n        console.log('ChartComponentRenderer: Set properties after DOM attachment');\n        console.log('ChartComponentRenderer: chartElement.data:', chartElement.data);\n        console.log('ChartComponentRenderer: chartElement.layout:', chartElement.layout);\n      });\n    } else {\n      // Fallback for invalid chart data\n      container.innerHTML = `\n        <div class=\"chart-error\">\n          <p>Invalid chart data format</p>\n          <pre>${JSON.stringify(component.data, null, 2).substring(0, 200)}...</pre>\n        </div>\n      `;\n    }\n\n    return container;\n  }\n}\n\n// Artifact component renderer\nexport class ArtifactComponentRenderer extends BaseComponentRenderer {\n  private defaultPrevented = false;\n\n  render(component: RichComponent): HTMLElement {\n    console.log('🔧 ArtifactComponentRenderer.render called with:', component);\n\n    const container = document.createElement('div');\n    container.className = 'rich-component rich-artifact';\n    container.dataset.componentId = component.id;\n    container.dataset.artifactId = component.data.artifact_id;\n\n    const {\n      content,\n      artifact_type,\n      title,\n      description,\n      editable,\n      fullscreen_capable,\n      external_renderable\n    } = component.data;\n\n    // Create artifact preview and controls\n    container.innerHTML = `\n      <div class=\"artifact-header\">\n        <div class=\"artifact-meta\">\n          <h3 class=\"artifact-title\">${title || 'Artifact'}</h3>\n          ${description ? `<p class=\"artifact-description\">${description}</p>` : ''}\n          <span class=\"artifact-type-badge\">${artifact_type}</span>\n        </div>\n        <div class=\"artifact-controls\">\n          ${editable ? '<button class=\"artifact-btn edit-btn\" title=\"Edit\">✏️</button>' : ''}\n          ${fullscreen_capable ? '<button class=\"artifact-btn fullscreen-btn\" title=\"Fullscreen\">⛶</button>' : ''}\n          ${external_renderable ? '<button class=\"artifact-btn external-btn\" title=\"Open External\">🔗</button>' : ''}\n        </div>\n      </div>\n      <div class=\"artifact-preview\">\n        <iframe class=\"artifact-iframe\" sandbox=\"allow-scripts allow-same-origin\" srcdoc=\"${this.escapeHtml(content)}\"></iframe>\n      </div>\n    `;\n\n    // Attach event listeners\n    this.attachEventListeners(container, component);\n\n    // Fire artifact-opened event for creation\n    const shouldRenderInChat = this.fireArtifactOpenedEvent(component, 'created', container);\n\n    // If default was prevented, show a placeholder instead\n    if (!shouldRenderInChat) {\n      container.innerHTML = `\n        <div class=\"artifact-placeholder\">\n          <div class=\"placeholder-content\">\n            <span class=\"placeholder-icon\">🎨</span>\n            <div class=\"placeholder-text\">\n              <strong>${title || 'Artifact'}</strong> opened externally\n              <div class=\"placeholder-type\">${artifact_type}</div>\n            </div>\n            <button class=\"placeholder-reopen\" title=\"Reopen\">↗</button>\n          </div>\n        </div>\n      `;\n\n      // Add reopen functionality\n      const reopenBtn = container.querySelector('.placeholder-reopen') as HTMLButtonElement;\n      if (reopenBtn) {\n        reopenBtn.addEventListener('click', () => {\n          this.fireArtifactOpenedEvent(component, 'user-action', container);\n        });\n      }\n    }\n\n    return container;\n  }\n\n  private attachEventListeners(container: HTMLElement, component: RichComponent): void {\n    // External button click\n    const externalBtn = container.querySelector('.external-btn') as HTMLButtonElement;\n    if (externalBtn) {\n      externalBtn.addEventListener('click', () => {\n        this.fireArtifactOpenedEvent(component, 'user-action', container);\n      });\n    }\n\n    // Fullscreen button click\n    const fullscreenBtn = container.querySelector('.fullscreen-btn') as HTMLButtonElement;\n    if (fullscreenBtn) {\n      fullscreenBtn.addEventListener('click', () => {\n        this.openFullscreen(component);\n      });\n    }\n\n    // Edit button click (placeholder for future implementation)\n    const editBtn = container.querySelector('.edit-btn') as HTMLButtonElement;\n    if (editBtn) {\n      editBtn.addEventListener('click', () => {\n        this.openEditor(component);\n      });\n    }\n  }\n\n  private fireArtifactOpenedEvent(component: RichComponent, trigger: 'created' | 'user-action', container: HTMLElement): boolean {\n    console.log('🎯 fireArtifactOpenedEvent called:', { trigger, artifactId: component.data.artifact_id });\n\n    this.defaultPrevented = false;\n\n    const eventDetail: ArtifactOpenedEventDetail = {\n      artifactId: component.data.artifact_id,\n      content: component.data.content,\n      type: component.data.artifact_type,\n      title: component.data.title,\n      description: component.data.description,\n      trigger,\n      preventDefault: () => {\n        console.log('🛑 preventDefault called!');\n        this.defaultPrevented = true;\n      },\n      getStandaloneHTML: () => this.generateStandaloneHTML(component),\n      timestamp: new Date().toISOString()\n    };\n\n    const event = new CustomEvent('artifact-opened', {\n      detail: eventDetail,\n      bubbles: true,\n      cancelable: true\n    });\n\n    console.log('📡 Dispatching artifact-opened event:', event);\n\n    // Fire the event from the container element (should bubble up to vanna-chat)\n    container.dispatchEvent(event);\n\n    // Also dispatch directly on the vanna-chat element as backup\n    const vannaChat = container.closest('vanna-chat');\n    if (vannaChat) {\n      console.log('📡 Also dispatching on vanna-chat element');\n      vannaChat.dispatchEvent(new CustomEvent('artifact-opened', {\n        detail: eventDetail,\n        bubbles: true,\n        cancelable: true\n      }));\n    }\n\n    console.log('📨 Event dispatched. defaultPrevented:', this.defaultPrevented);\n\n    // Handle default behavior if not prevented and user triggered\n    if (!this.defaultPrevented && trigger === 'user-action') {\n      this.handleDefaultAction(component);\n    }\n\n    // Return whether we should render in chat (true if default not prevented)\n    return !this.defaultPrevented;\n  }\n\n  private generateStandaloneHTML(component: RichComponent): string {\n    const { content, title, dependencies = [] } = component.data;\n\n    let dependenciesHTML = '';\n\n    // Add common CDN links for dependencies\n    if (dependencies.includes('d3')) {\n      dependenciesHTML += '<script src=\"https://d3js.org/d3.v7.min.js\"></script>\\n';\n    }\n    if (dependencies.includes('plotly')) {\n      dependenciesHTML += '<script src=\"https://cdn.plot.ly/plotly-latest.min.js\"></script>\\n';\n    }\n    if (dependencies.includes('three') || dependencies.includes('threejs')) {\n      dependenciesHTML += '<script src=\"https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js\"></script>\\n';\n    }\n\n    return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>${title || 'Artifact'}</title>\n    ${dependenciesHTML}\n    <style>\n        body {\n            margin: 0;\n            padding: 20px;\n            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n        }\n        .artifact-container {\n            width: 100%;\n            min-height: 100vh;\n        }\n    </style>\n</head>\n<body>\n    <div class=\"artifact-container\">\n        ${content}\n    </div>\n</body>\n</html>`;\n  }\n\n  private handleDefaultAction(component: RichComponent): void {\n    // Default action: open in new window\n    const newWindow = window.open('', '_blank', 'width=800,height=600');\n    if (newWindow) {\n      newWindow.document.write(this.generateStandaloneHTML(component));\n      newWindow.document.close();\n    }\n  }\n\n  private openFullscreen(component: RichComponent): void {\n    // Create fullscreen overlay\n    const overlay = document.createElement('div');\n    overlay.className = 'artifact-fullscreen-overlay';\n    overlay.innerHTML = `\n      <div class=\"fullscreen-header\">\n        <h3>${component.data.title || 'Artifact'}</h3>\n        <button class=\"close-fullscreen\">✕</button>\n      </div>\n      <div class=\"fullscreen-content\">\n        <iframe class=\"fullscreen-iframe\" sandbox=\"allow-scripts allow-same-origin\" srcdoc=\"${this.escapeHtml(component.data.content)}\"></iframe>\n      </div>\n    `;\n\n    // Add styles\n    overlay.style.cssText = `\n      position: fixed;\n      top: 0;\n      left: 0;\n      width: 100vw;\n      height: 100vh;\n      background: white;\n      z-index: 10000;\n      display: flex;\n      flex-direction: column;\n    `;\n\n    const header = overlay.querySelector('.fullscreen-header') as HTMLElement;\n    header.style.cssText = `\n      padding: 16px;\n      border-bottom: 1px solid #eee;\n      display: flex;\n      justify-content: space-between;\n      align-items: center;\n    `;\n\n    const content = overlay.querySelector('.fullscreen-content') as HTMLElement;\n    content.style.cssText = `\n      flex: 1;\n      padding: 16px;\n    `;\n\n    const iframe = overlay.querySelector('.fullscreen-iframe') as HTMLIFrameElement;\n    iframe.style.cssText = `\n      width: 100%;\n      height: 100%;\n      border: none;\n    `;\n\n    // Close button functionality\n    const closeBtn = overlay.querySelector('.close-fullscreen') as HTMLButtonElement;\n    closeBtn.addEventListener('click', () => {\n      document.body.removeChild(overlay);\n    });\n\n    // Escape key to close\n    const handleEscape = (e: KeyboardEvent) => {\n      if (e.key === 'Escape') {\n        document.body.removeChild(overlay);\n        document.removeEventListener('keydown', handleEscape);\n      }\n    };\n    document.addEventListener('keydown', handleEscape);\n\n    document.body.appendChild(overlay);\n  }\n\n  private openEditor(component: RichComponent): void {\n    // Placeholder for future editor implementation\n    console.log('Editor functionality not yet implemented for artifact:', component.data.artifact_id);\n  }\n\n  private escapeHtml(html: string): string {\n    const div = document.createElement('div');\n    div.textContent = html;\n    return div.innerHTML.replace(/\"/g, '&quot;');\n  }\n}\n\n// User message component renderer\nexport class UserMessageComponentRenderer extends BaseComponentRenderer {\n  render(component: RichComponent): HTMLElement {\n    const messageEl = document.createElement('vanna-message');\n    messageEl.setAttribute('theme', 'light'); // Could be made dynamic\n    messageEl.dataset.componentId = component.id;\n    \n    // Set properties for vanna-message\n    (messageEl as any).content = component.data.content || '';\n    (messageEl as any).type = 'user';\n    (messageEl as any).timestamp = Date.parse(component.timestamp);\n    \n    return messageEl;\n  }\n}\n\n// Assistant message component renderer  \nexport class AssistantMessageComponentRenderer extends BaseComponentRenderer {\n  render(component: RichComponent): HTMLElement {\n    const messageEl = document.createElement('vanna-message');\n    messageEl.setAttribute('theme', 'light'); // Could be made dynamic\n    messageEl.dataset.componentId = component.id;\n    \n    // Set properties for vanna-message\n    (messageEl as any).content = component.data.content || '';\n    (messageEl as any).type = 'assistant';\n    (messageEl as any).timestamp = Date.parse(component.timestamp);\n    \n    return messageEl;\n  }\n}\n\n// Component registry for managing all component types\nexport class ComponentRegistry {\n  private renderers: Map<string, ComponentRenderer> = new Map();\n\n  constructor() {\n    // Register primitive component renderers (domain-agnostic)\n    this.register('status_card', new StatusCardComponentRenderer());\n    this.register('progress_display', new ProgressDisplayComponentRenderer());\n    this.register('log_viewer', new LogViewerComponentRenderer());\n    this.register('badge', new BadgeComponentRenderer());\n    this.register('icon_text', new IconTextComponentRenderer());\n\n    // Register existing component renderers\n    this.register('card', new CardComponentRenderer());\n    this.register('task_list', new TaskListComponentRenderer());\n    this.register('progress_bar', new ProgressBarComponentRenderer());\n    this.register('notification', new NotificationComponentRenderer());\n    this.register('status_indicator', new StatusIndicatorComponentRenderer());\n    this.register('text', new TextComponentRenderer());\n    this.register('dataframe', new DataFrameComponentRenderer());\n    this.register('chart', new ChartComponentRenderer());\n\n    // Register interactive component renderers\n    this.register('button', new ButtonComponentRenderer());\n    this.register('button_group', new ButtonGroupComponentRenderer());\n\n    // Register artifact component renderer\n    this.register('artifact', new ArtifactComponentRenderer());\n\n    // Register message component renderers\n    this.register('user-message', new UserMessageComponentRenderer());\n    this.register('assistant-message', new AssistantMessageComponentRenderer());\n  }\n\n  register(type: string, renderer: ComponentRenderer): void {\n    this.renderers.set(type, renderer);\n  }\n\n  render(component: RichComponent): HTMLElement {\n    // Check if this is a component that should use web components\n    const webComponentTag = this.getWebComponentTag(component.type);\n    if (webComponentTag) {\n      return this.renderWebComponent(webComponentTag, component);\n    }\n\n    // Use the old renderer system for other components\n    const renderer = this.renderers.get(component.type);\n    if (!renderer) {\n      return this.renderFallback(component);\n    }\n    return renderer.render(component);\n  }\n\n  private getWebComponentTag(type: string): string | null {\n    const mapping: Record<string, string> = {\n      'card': 'rich-card',\n      'task_list': 'rich-task-list',\n      'progress_bar': 'rich-progress-bar',\n      // We'll add more mappings as we convert other components\n    };\n    return mapping[type] || null;\n  }\n\n  private renderWebComponent(tagName: string, component: RichComponent): HTMLElement {\n    const element = document.createElement(tagName) as any;\n\n    // Set properties based on component data\n    Object.keys(component.data).forEach(key => {\n      if (key === 'actions' && Array.isArray(component.data[key])) {\n        element.actions = component.data[key];\n      } else {\n        element[key] = component.data[key];\n      }\n    });\n\n    // Set theme to match the parent VannaChat theme\n    element.setAttribute('theme', this.getCurrentTheme());\n\n\n    return element;\n  }\n\n  private getCurrentTheme(): string {\n    // Try to get theme from the parent VannaChat component\n    const vannaChat = document.querySelector('vanna-chat');\n    if (vannaChat) {\n      return vannaChat.getAttribute('theme') || 'dark';\n    }\n    return 'dark';\n  }\n\n\n  update(element: HTMLElement, component: RichComponent, updates?: Record<string, any>): void {\n    const renderer = this.renderers.get(component.type);\n    if (renderer) {\n      renderer.update(element, component, updates);\n    }\n  }\n\n  remove(element: HTMLElement): void {\n    element.remove();\n  }\n\n  private renderFallback(component: RichComponent): HTMLElement {\n    const container = document.createElement('div');\n    container.className = 'rich-component rich-fallback';\n    container.dataset.componentId = component.id;\n\n    container.innerHTML = `\n      <div class=\"fallback-header\">\n        <strong>Unknown Component: ${component.type}</strong>\n      </div>\n      <pre class=\"fallback-data\">${JSON.stringify(component.data, null, 2)}</pre>\n    `;\n\n    return container;\n  }\n}\n\n// Component manager for handling component lifecycle\nexport class ComponentManager {\n  private components: Map<string, RichComponent> = new Map();\n  private elements: Map<string, HTMLElement> = new Map();\n  private registry: ComponentRegistry = new ComponentRegistry();\n  private container: HTMLElement;\n  private readonly sharedFields = new Set([\n    'id',\n    'type',\n    'lifecycle',\n    'layout',\n    'theme',\n    'children',\n    'timestamp',\n    'visible',\n    'interactive',\n  ]);\n\n  constructor(container: HTMLElement) {\n    this.container = container;\n    ensureRichComponentStyles(this.container);\n  }\n\n  processUpdate(update: ComponentUpdate): void {\n    // Handle UI state updates with special processing\n    if (update.component && this.isUIStateUpdate(update.component)) {\n      this.processUIStateUpdate(update.component);\n      return;\n    }\n\n    switch (update.operation) {\n      case 'create':\n        this.createComponent(update);\n        break;\n      case 'update':\n        this.updateComponent(update);\n        break;\n      case 'replace':\n        this.replaceComponent(update);\n        break;\n      case 'remove':\n        this.removeComponent(update);\n        break;\n    }\n  }\n\n  private createComponent(update: ComponentUpdate): void {\n    if (!update.component) return;\n\n    const component = this.normalizeComponent(update.component);\n    const element = this.registry.render(component);\n    this.components.set(component.id, component);\n    this.elements.set(component.id, element);\n\n    // Determine where to place the component\n    this.positionComponent(element);\n  }\n\n  private updateComponent(update: ComponentUpdate): void {\n    if (!update.component) return;\n\n    const element = this.elements.get(update.target_id);\n    if (element) {\n      const component = this.normalizeComponent(update.component);\n      this.registry.update(element, component, update.updates);\n      this.components.set(update.target_id, component);\n    }\n  }\n\n  private replaceComponent(update: ComponentUpdate): void {\n    if (!update.component) return;\n\n    const oldElement = this.elements.get(update.target_id);\n    if (oldElement) {\n      const component = this.normalizeComponent(update.component);\n      const newElement = this.registry.render(component);\n      oldElement.parentNode?.replaceChild(newElement, oldElement);\n\n      this.elements.set(component.id, newElement);\n      this.components.set(component.id, component);\n\n      // Clean up old references if ID changed\n      if (update.target_id !== component.id) {\n        this.elements.delete(update.target_id);\n        this.components.delete(update.target_id);\n      }\n    }\n  }\n\n  private removeComponent(update: ComponentUpdate): void {\n    const element = this.elements.get(update.target_id);\n    if (element) {\n      element.remove();\n      this.elements.delete(update.target_id);\n      this.components.delete(update.target_id);\n    }\n  }\n\n  private positionComponent(element: HTMLElement): void {\n    // Always append to container\n    this.container.appendChild(element);\n\n    // Trigger scroll to bottom in parent chat component\n    this.triggerScroll();\n  }\n\n  private triggerScroll(): void {\n    // Find the parent vanna-chat component and trigger its scroll method\n    const vannaChat = document.querySelector('vanna-chat') as any;\n    if (vannaChat && typeof vannaChat.scrollToLastMessage === 'function') {\n      // Use requestAnimationFrame to wait for DOM update\n      requestAnimationFrame(() => {\n        requestAnimationFrame(() => {\n          vannaChat.scrollToLastMessage();\n        });\n      });\n    }\n  }\n\n  clear(): void {\n    this.components.clear();\n    this.elements.clear();\n    this.container.innerHTML = '';\n    ensureRichComponentStyles(this.container);\n  }\n\n  getComponent(id: string): RichComponent | undefined {\n    return this.components.get(id);\n  }\n\n  getAllComponents(): RichComponent[] {\n    return Array.from(this.components.values());\n  }\n\n  private normalizeComponent(component: RichComponent): RichComponent {\n    const data = { ...(component.data ?? {}) };\n\n    for (const [key, value] of Object.entries(component as Record<string, any>)) {\n      if (this.sharedFields.has(key) || key === 'data') continue;\n      data[key] = value;\n    }\n\n    if (component.data && Object.keys(component.data).length === Object.keys(data).length) {\n      return component;\n    }\n\n    return {\n      ...component,\n      data,\n    };\n  }\n\n  private isUIStateUpdate(component: RichComponent): boolean {\n    return component.type === 'status_bar_update' ||\n           component.type === 'task_tracker_update' ||\n           component.type === 'chat_input_update';\n  }\n\n  private processUIStateUpdate(component: RichComponent): void {\n    console.log('processUIStateUpdate called with type:', component.type, 'component:', component);\n\n    switch (component.type) {\n      case 'status_bar_update':\n        this.updateStatusBar(component);\n        break;\n      case 'task_tracker_update':\n        this.updateTaskTracker(component);\n        break;\n      case 'chat_input_update':\n        this.updateChatInput(component);\n        break;\n    }\n  }\n\n  private updateStatusBar(component: RichComponent): void {\n    // Find the status bar component - first try shadow DOM, then document\n    let statusBar: HTMLElement | null = null;\n\n    // Look for vanna-chat and search within its shadow root\n    const vannaChat = document.querySelector('vanna-chat') as any;\n    if (vannaChat && vannaChat.shadowRoot) {\n      statusBar = vannaChat.shadowRoot.querySelector('vanna-status-bar') as HTMLElement | null;\n    }\n\n    // Fallback to document search\n    if (!statusBar) {\n      statusBar = document.querySelector('vanna-status-bar') as HTMLElement | null;\n    }\n\n    if (statusBar) {\n      const { status, message, detail } = component.data || {};\n      // Set properties directly on the Lit component\n      (statusBar as any).status = status;\n      (statusBar as any).message = message || '';\n      (statusBar as any).detail = detail || '';\n    }\n  }\n\n  private updateTaskTracker(component: RichComponent): void {\n    // Debug logging\n    console.log('updateTaskTracker called with component:', component);\n    console.log('component.data:', component.data);\n\n    // Find the progress tracker component - first try shadow DOM, then document\n    let progressTracker = null;\n\n    // Look for vanna-chat and search within its shadow root\n    const vannaChat = document.querySelector('vanna-chat') as any;\n    if (vannaChat && vannaChat.shadowRoot) {\n      progressTracker = vannaChat.shadowRoot.querySelector('vanna-progress-tracker');\n    }\n\n    // Fallback to document search\n    if (!progressTracker) {\n      progressTracker = document.querySelector('vanna-progress-tracker');\n    }\n\n    console.log('Found progressTracker:', progressTracker);\n    if (!progressTracker) return;\n\n    const { operation, task, task_id, status, detail } = component.data || {};\n    console.log('Extracted data:', { operation, task, task_id, status, detail });\n\n    switch (operation) {\n      case 'add_task':\n        console.log('Adding task:', task);\n        if (task && progressTracker.addItem) {\n          // Use the backend task ID instead of generating a new one\n          const result = progressTracker.addItem(task.title || task.text, task.description || task.detail, task.id);\n          console.log('addItem result:', result, 'using backend ID:', task.id);\n        }\n        break;\n      case 'update_task':\n        console.log('Updating task:', task_id, status, detail);\n        if (task_id && progressTracker.updateItem) {\n          progressTracker.updateItem(task_id, status, detail);\n        }\n        break;\n      case 'remove_task':\n        if (task_id && progressTracker.removeItem) {\n          progressTracker.removeItem(task_id);\n        }\n        break;\n      case 'clear_tasks':\n        if (progressTracker.clear) {\n          progressTracker.clear();\n        }\n        break;\n    }\n  }\n\n  private updateChatInput(component: RichComponent): void {\n    // Find the chat input element - first try shadow DOM, then document\n    let chatInput = null;\n\n    // Look for vanna-chat and search within its shadow root\n    const vannaChat = document.querySelector('vanna-chat') as any;\n    if (vannaChat && vannaChat.shadowRoot) {\n      chatInput = vannaChat.shadowRoot.querySelector('textarea.message-input, input.message-input');\n    }\n\n    // Fallback to document search with multiple selectors\n    if (!chatInput) {\n      chatInput = document.querySelector('textarea[data-testid=\"message-input\"], input[type=\"text\"].message-input, .message-input input, .message-input textarea');\n    }\n\n    if (!chatInput) return;\n\n    const { placeholder, disabled, value, focus } = component.data || {};\n\n    if (placeholder !== undefined) {\n      chatInput.placeholder = placeholder;\n    }\n    if (disabled !== undefined) {\n      chatInput.disabled = disabled;\n    }\n    if (value !== undefined) {\n      chatInput.value = value;\n    }\n    if (focus !== undefined) {\n      if (focus) {\n        chatInput.focus();\n      } else {\n        chatInput.blur();\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontends/webcomponent/src/components/rich-progress-bar.stories.ts",
    "content": "import type { Meta, StoryObj } from '@storybook/web-components';\nimport { html } from 'lit';\nimport './rich-progress-bar';\n\nconst meta: Meta = {\n  title: 'Rich Components/Rich Progress Bar',\n  component: 'rich-progress-bar',\n  parameters: {\n    layout: 'padded',\n    backgrounds: {\n      default: 'light',\n      values: [\n        { name: 'dark', value: 'rgb(11, 15, 25)' },\n        { name: 'light', value: '#f5f7fa' },\n      ],\n    },\n  },\n  argTypes: {\n    value: { control: { type: 'range', min: 0, max: 1, step: 0.01 } },\n    label: { control: 'text' },\n    description: { control: 'text' },\n    showPercentage: { control: 'boolean' },\n    status: {\n      control: 'select',\n      options: ['info', 'success', 'warning', 'error']\n    },\n    animated: { control: 'boolean' },\n    indeterminate: { control: 'boolean' },\n  },\n};\n\nexport default meta;\ntype Story = StoryObj;\n\nexport const Default: Story = {\n  args: {\n    value: 0.65,\n    label: 'Processing',\n    showPercentage: true,\n    status: 'info',\n    animated: false,\n    indeterminate: false,\n  },\n  render: (args) => html`\n    <div style=\"max-width: 500px; margin: 0 auto;\">\n      <rich-progress-bar\n        .value=${args.value}\n        label=${args.label}\n        description=${args.description}\n        ?showPercentage=${args.showPercentage}\n        status=${args.status}\n        ?animated=${args.animated}\n        ?indeterminate=${args.indeterminate}>\n      </rich-progress-bar>\n    </div>\n  `,\n};\n\nexport const WithDescription: Story = {\n  args: {\n    value: 0.4,\n    label: 'Installing dependencies',\n    description: 'Downloading and installing npm packages for the project. This may take a few minutes.',\n    showPercentage: true,\n    status: 'info',\n    animated: true,\n    indeterminate: false,\n  },\n  render: (args) => html`\n    <div style=\"max-width: 500px; margin: 0 auto;\">\n      <rich-progress-bar\n        .value=${args.value}\n        label=${args.label}\n        description=${args.description}\n        ?showPercentage=${args.showPercentage}\n        status=${args.status}\n        ?animated=${args.animated}\n        ?indeterminate=${args.indeterminate}>\n      </rich-progress-bar>\n    </div>\n  `,\n};\n\nexport const Animated: Story = {\n  args: {\n    value: 0.75,\n    label: 'Uploading files',\n    showPercentage: true,\n    status: 'info',\n    animated: true,\n    indeterminate: false,\n  },\n  render: (args) => html`\n    <div style=\"max-width: 500px; margin: 0 auto;\">\n      <rich-progress-bar\n        .value=${args.value}\n        label=${args.label}\n        description=${args.description}\n        ?showPercentage=${args.showPercentage}\n        status=${args.status}\n        ?animated=${args.animated}\n        ?indeterminate=${args.indeterminate}>\n      </rich-progress-bar>\n    </div>\n  `,\n};\n\nexport const Indeterminate: Story = {\n  args: {\n    value: 0,\n    label: 'Loading...',\n    description: 'Please wait while we process your request',\n    showPercentage: false,\n    status: 'info',\n    animated: false,\n    indeterminate: true,\n  },\n  render: (args) => html`\n    <div style=\"max-width: 500px; margin: 0 auto;\">\n      <rich-progress-bar\n        .value=${args.value}\n        label=${args.label}\n        description=${args.description}\n        ?showPercentage=${args.showPercentage}\n        status=${args.status}\n        ?animated=${args.animated}\n        ?indeterminate=${args.indeterminate}>\n      </rich-progress-bar>\n    </div>\n  `,\n};\n\nexport const StatusVariants: Story = {\n  render: () => html`\n    <div style=\"max-width: 500px; margin: 0 auto; display: flex; flex-direction: column; gap: 16px;\">\n      <rich-progress-bar\n        .value=${0.8}\n        label=\"Info Status\"\n        status=\"info\"\n        showPercentage>\n      </rich-progress-bar>\n\n      <rich-progress-bar\n        .value=${1.0}\n        label=\"Success Status\"\n        status=\"success\"\n        showPercentage>\n      </rich-progress-bar>\n\n      <rich-progress-bar\n        .value=${0.6}\n        label=\"Warning Status\"\n        status=\"warning\"\n        showPercentage>\n      </rich-progress-bar>\n\n      <rich-progress-bar\n        .value=${0.3}\n        label=\"Error Status\"\n        status=\"error\"\n        showPercentage>\n      </rich-progress-bar>\n    </div>\n  `,\n};\n\nexport const Minimal: Story = {\n  args: {\n    value: 0.45,\n    showPercentage: false,\n    status: 'info',\n    animated: false,\n    indeterminate: false,\n  },\n  render: (args) => html`\n    <div style=\"max-width: 500px; margin: 0 auto;\">\n      <rich-progress-bar\n        .value=${args.value}\n        ?showPercentage=${args.showPercentage}\n        status=${args.status}\n        ?animated=${args.animated}\n        ?indeterminate=${args.indeterminate}>\n      </rich-progress-bar>\n    </div>\n  `,\n};\n\nexport const MultipleSteps: Story = {\n  render: () => html`\n    <div style=\"max-width: 500px; margin: 0 auto; display: flex; flex-direction: column; gap: 16px;\">\n      <rich-progress-bar\n        .value=${1.0}\n        label=\"Step 1: Initialize\"\n        description=\"Project initialization completed\"\n        status=\"success\"\n        showPercentage>\n      </rich-progress-bar>\n\n      <rich-progress-bar\n        .value=${1.0}\n        label=\"Step 2: Download dependencies\"\n        description=\"All packages downloaded successfully\"\n        status=\"success\"\n        showPercentage>\n      </rich-progress-bar>\n\n      <rich-progress-bar\n        .value=${0.7}\n        label=\"Step 3: Build project\"\n        description=\"Compiling TypeScript and bundling assets\"\n        status=\"info\"\n        animated\n        showPercentage>\n      </rich-progress-bar>\n\n      <rich-progress-bar\n        .value=${0}\n        label=\"Step 4: Deploy\"\n        description=\"Waiting for build to complete\"\n        status=\"info\"\n        showPercentage>\n      </rich-progress-bar>\n    </div>\n  `,\n};\n\nexport const LightTheme: Story = {\n  args: {\n    value: 0.55,\n    label: 'Light Theme Progress',\n    description: 'Progress bar styled for light backgrounds',\n    showPercentage: true,\n    status: 'success',\n    animated: true,\n  },\n  parameters: {\n    backgrounds: { default: 'light' }\n  },\n  render: (args) => html`\n    <div style=\"max-width: 500px; margin: 0 auto;\">\n      <rich-progress-bar\n        theme=\"light\"\n        .value=${args.value}\n        label=${args.label}\n        description=${args.description}\n        ?showPercentage=${args.showPercentage}\n        status=${args.status}\n        ?animated=${args.animated}>\n      </rich-progress-bar>\n    </div>\n  `,\n};"
  },
  {
    "path": "frontends/webcomponent/src/components/rich-progress-bar.ts",
    "content": "import { LitElement, html, css } from 'lit';\nimport { customElement, property } from 'lit/decorators.js';\nimport { vannaDesignTokens } from '../styles/vanna-design-tokens.js';\n\n@customElement('rich-progress-bar')\nexport class RichProgressBar extends LitElement {\n  static styles = [\n    vannaDesignTokens,\n    css`\n      :host {\n        display: block;\n        margin-bottom: var(--vanna-space-4);\n        font-family: var(--vanna-font-family-default);\n      }\n\n      .progress-container {\n        padding: var(--vanna-space-4);\n        border: 1px solid var(--vanna-outline-default);\n        border-radius: var(--vanna-border-radius-lg);\n        background: var(--vanna-background-default);\n        box-shadow: var(--vanna-shadow-sm);\n        transition: box-shadow var(--vanna-duration-200) ease;\n      }\n\n      .progress-container:hover {\n        box-shadow: var(--vanna-shadow-md);\n      }\n\n      .progress-header {\n        display: flex;\n        justify-content: space-between;\n        align-items: center;\n        margin-bottom: var(--vanna-space-3);\n      }\n\n      .progress-label {\n        font-weight: 500;\n        color: var(--vanna-foreground-default);\n      }\n\n      .progress-percentage {\n        font-size: 0.875rem;\n        color: var(--vanna-foreground-dimmer);\n        font-weight: 600;\n      }\n\n      .progress-track {\n        height: 12px;\n        background: var(--vanna-background-root);\n        border-radius: 6px;\n        overflow: hidden;\n        border: 1px solid var(--vanna-outline-default);\n        position: relative;\n      }\n\n      .progress-fill {\n        height: 100%;\n        background: var(--vanna-accent-primary-default);\n        border-radius: 6px;\n        transition: width var(--vanna-duration-300) ease;\n        position: relative;\n        overflow: hidden;\n      }\n\n      .progress-fill.animated {\n        animation: progressPulse 2s ease-in-out infinite;\n      }\n\n      .progress-fill.animated::after {\n        content: '';\n        position: absolute;\n        top: 0;\n        left: 0;\n        bottom: 0;\n        right: 0;\n        background: linear-gradient(\n          90deg,\n          transparent,\n          rgba(255, 255, 255, 0.2),\n          transparent\n        );\n        animation: progressShimmer 1.5s infinite;\n      }\n\n      @keyframes progressPulse {\n        0%, 100% { opacity: 1; }\n        50% { opacity: 0.8; }\n      }\n\n      @keyframes progressShimmer {\n        0% { transform: translateX(-100%); }\n        100% { transform: translateX(100%); }\n      }\n\n      .progress-fill.status-success {\n        background: var(--vanna-accent-positive-default);\n      }\n\n      .progress-fill.status-warning {\n        background: var(--vanna-accent-warning-default);\n      }\n\n      .progress-fill.status-error {\n        background: var(--vanna-accent-negative-default);\n      }\n\n      .progress-fill.status-info {\n        background: var(--vanna-accent-primary-default);\n      }\n\n      /* Indeterminate progress animation */\n      .progress-fill.indeterminate {\n        background: linear-gradient(\n          90deg,\n          transparent 0%,\n          var(--vanna-accent-primary-default) 50%,\n          transparent 100%\n        );\n        background-size: 200% 100%;\n        animation: indeterminateProgress 2s linear infinite;\n        width: 100% !important;\n      }\n\n      @keyframes indeterminateProgress {\n        0% { background-position: 200% 0; }\n        100% { background-position: -200% 0; }\n      }\n\n      /* Text content for description */\n      .progress-description {\n        margin-top: var(--vanna-space-2);\n        font-size: 0.875rem;\n        color: var(--vanna-foreground-dimmer);\n        line-height: 1.4;\n      }\n    `\n  ];\n\n  @property({ type: Number }) value = 0;\n  @property() label = '';\n  @property() description = '';\n  @property({ type: Boolean }) showPercentage = true;\n  @property() status: 'info' | 'success' | 'warning' | 'error' = 'info';\n  @property({ type: Boolean }) animated = false;\n  @property({ type: Boolean }) indeterminate = false;\n  @property() theme: 'light' | 'dark' = 'dark';\n\n  private get percentage(): number {\n    if (this.indeterminate) return 100;\n    return Math.round(Math.max(0, Math.min(1, this.value)) * 100);\n  }\n\n  private get progressClasses(): string {\n    const classes = ['progress-fill'];\n\n    if (this.animated) {\n      classes.push('animated');\n    }\n\n    if (this.indeterminate) {\n      classes.push('indeterminate');\n    }\n\n    if (this.status) {\n      classes.push(`status-${this.status}`);\n    }\n\n    return classes.join(' ');\n  }\n\n  render() {\n    return html`\n      <div class=\"progress-container\">\n        ${this.label || this.showPercentage ? html`\n          <div class=\"progress-header\">\n            ${this.label ? html`<span class=\"progress-label\">${this.label}</span>` : ''}\n            ${this.showPercentage && !this.indeterminate ? html`\n              <span class=\"progress-percentage\">${this.percentage}%</span>\n            ` : ''}\n          </div>\n        ` : ''}\n\n        <div class=\"progress-track\">\n          <div\n            class=\"${this.progressClasses}\"\n            style=\"width: ${this.indeterminate ? '100' : this.percentage}%\">\n          </div>\n        </div>\n\n        ${this.description ? html`\n          <div class=\"progress-description\">${this.description}</div>\n        ` : ''}\n      </div>\n    `;\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'rich-progress-bar': RichProgressBar;\n  }\n}"
  },
  {
    "path": "frontends/webcomponent/src/components/rich-task-list.stories.ts",
    "content": "import type { Meta, StoryObj } from '@storybook/web-components';\nimport { html } from 'lit';\nimport './rich-task-list';\n\nconst meta: Meta = {\n  title: 'Rich Components/Rich Task List',\n  component: 'rich-task-list',\n  parameters: {\n    layout: 'padded',\n    backgrounds: {\n      default: 'light',\n      values: [\n        { name: 'dark', value: 'rgb(11, 15, 25)' },\n        { name: 'light', value: '#f5f7fa' },\n      ],\n    },\n  },\n  argTypes: {\n    title: { control: 'text' },\n    tasks: { control: 'object' },\n    showProgress: { control: 'boolean' },\n    showTimestamps: { control: 'boolean' },\n  },\n};\n\nexport default meta;\ntype Story = StoryObj;\n\nconst sampleTasks = [\n  {\n    id: '1',\n    title: 'Initialize project setup',\n    description: 'Setting up the basic project structure and dependencies',\n    status: 'completed',\n    progress: 1.0,\n    timestamp: '2024-01-15 10:30:00'\n  },\n  {\n    id: '2',\n    title: 'Configure database connection',\n    description: 'Establishing secure connection to PostgreSQL database',\n    status: 'completed',\n    progress: 1.0,\n    timestamp: '2024-01-15 10:45:00'\n  },\n  {\n    id: '3',\n    title: 'Implement user authentication',\n    description: 'Building JWT-based authentication system',\n    status: 'running',\n    progress: 0.7,\n    timestamp: '2024-01-15 11:00:00'\n  },\n  {\n    id: '4',\n    title: 'Create API endpoints',\n    description: 'Developing RESTful API for user management',\n    status: 'pending',\n    timestamp: '2024-01-15 11:30:00'\n  },\n  {\n    id: '5',\n    title: 'Write unit tests',\n    description: 'Comprehensive test coverage for all modules',\n    status: 'pending',\n  }\n];\n\nexport const Default: Story = {\n  args: {\n    title: 'Development Tasks',\n    tasks: sampleTasks,\n    showProgress: true,\n    showTimestamps: false,\n  },\n  render: (args) => html`\n    <div style=\"max-width: 600px; margin: 0 auto;\">\n      <rich-task-list\n        title=${args.title}\n        .tasks=${args.tasks}\n        ?showProgress=${args.showProgress}\n        ?showTimestamps=${args.showTimestamps}>\n      </rich-task-list>\n    </div>\n  `,\n};\n\nexport const WithTimestamps: Story = {\n  args: {\n    title: 'Build Pipeline',\n    tasks: sampleTasks,\n    showProgress: true,\n    showTimestamps: true,\n  },\n  render: (args) => html`\n    <div style=\"max-width: 600px; margin: 0 auto;\">\n      <rich-task-list\n        title=${args.title}\n        .tasks=${args.tasks}\n        ?showProgress=${args.showProgress}\n        ?showTimestamps=${args.showTimestamps}>\n      </rich-task-list>\n    </div>\n  `,\n};\n\nexport const WithoutProgress: Story = {\n  args: {\n    title: 'Simple Task List',\n    tasks: [\n      { id: '1', title: 'Review code changes', status: 'completed' },\n      { id: '2', title: 'Update documentation', status: 'running' },\n      { id: '3', title: 'Deploy to staging', status: 'pending' },\n      { id: '4', title: 'Run integration tests', status: 'failed' },\n    ],\n    showProgress: false,\n    showTimestamps: false,\n  },\n  render: (args) => html`\n    <div style=\"max-width: 600px; margin: 0 auto;\">\n      <rich-task-list\n        title=${args.title}\n        .tasks=${args.tasks}\n        ?showProgress=${args.showProgress}\n        ?showTimestamps=${args.showTimestamps}>\n      </rich-task-list>\n    </div>\n  `,\n};\n\nexport const AllStatuses: Story = {\n  args: {\n    title: 'Task Status Examples',\n    tasks: [\n      {\n        id: '1',\n        title: 'Completed Task',\n        description: 'This task has been successfully completed',\n        status: 'completed',\n        progress: 1.0,\n      },\n      {\n        id: '2',\n        title: 'Running Task',\n        description: 'This task is currently in progress',\n        status: 'running',\n        progress: 0.6,\n      },\n      {\n        id: '3',\n        title: 'Pending Task',\n        description: 'This task is waiting to be started',\n        status: 'pending',\n      },\n      {\n        id: '4',\n        title: 'Failed Task',\n        description: 'This task encountered an error',\n        status: 'failed',\n        progress: 0.3,\n      },\n    ],\n    showProgress: true,\n    showTimestamps: false,\n  },\n  render: (args) => html`\n    <div style=\"max-width: 600px; margin: 0 auto;\">\n      <rich-task-list\n        title=${args.title}\n        .tasks=${args.tasks}\n        ?showProgress=${args.showProgress}\n        ?showTimestamps=${args.showTimestamps}>\n      </rich-task-list>\n    </div>\n  `,\n};\n\nexport const EmptyList: Story = {\n  args: {\n    title: 'No Tasks',\n    tasks: [],\n    showProgress: true,\n    showTimestamps: false,\n  },\n  render: (args) => html`\n    <div style=\"max-width: 600px; margin: 0 auto;\">\n      <rich-task-list\n        title=${args.title}\n        .tasks=${args.tasks}\n        ?showProgress=${args.showProgress}\n        ?showTimestamps=${args.showTimestamps}>\n      </rich-task-list>\n    </div>\n  `,\n};\n\nexport const ErrorStates: Story = {\n  args: {\n    title: 'Error Handling Examples',\n    tasks: [\n      {\n        id: '1',\n        title: 'Database Connection Failed',\n        description: 'Could not establish connection to the database server. Check network connectivity and credentials.',\n        status: 'failed',\n        progress: 0.1,\n        timestamp: '2024-01-15 10:15:00'\n      },\n      {\n        id: '2',\n        title: 'API Authentication Error',\n        description: 'Invalid API key or expired token. Please refresh your credentials.',\n        status: 'failed',\n        progress: 0.0,\n        timestamp: '2024-01-15 10:20:00'\n      },\n      {\n        id: '3',\n        title: 'File Processing Error',\n        description: 'Unable to process uploaded file. File may be corrupted or in an unsupported format.',\n        status: 'failed',\n        progress: 0.45,\n        timestamp: '2024-01-15 10:25:00'\n      },\n      {\n        id: '4',\n        title: 'Network Timeout',\n        description: 'Request timed out after 30 seconds. This may be due to high server load.',\n        status: 'failed',\n        progress: 0.8,\n        timestamp: '2024-01-15 10:30:00'\n      },\n    ],\n    showProgress: true,\n    showTimestamps: true,\n  },\n  render: (args) => html`\n    <div style=\"max-width: 600px; margin: 0 auto;\">\n      <rich-task-list\n        title=${args.title}\n        .tasks=${args.tasks}\n        ?showProgress=${args.showProgress}\n        ?showTimestamps=${args.showTimestamps}>\n      </rich-task-list>\n    </div>\n  `,\n};\n\nexport const LightTheme: Story = {\n  args: {\n    title: 'Light Theme Task List',\n    tasks: sampleTasks.slice(0, 3),\n    showProgress: true,\n    showTimestamps: true,\n  },\n  parameters: {\n    backgrounds: { default: 'light' }\n  },\n  render: (args) => html`\n    <div style=\"max-width: 600px; margin: 0 auto;\">\n      <rich-task-list\n        theme=\"light\"\n        title=${args.title}\n        .tasks=${args.tasks}\n        ?showProgress=${args.showProgress}\n        ?showTimestamps=${args.showTimestamps}>\n      </rich-task-list>\n    </div>\n  `,\n};"
  },
  {
    "path": "frontends/webcomponent/src/components/rich-task-list.ts",
    "content": "import { LitElement, html, css } from 'lit';\nimport { customElement, property } from 'lit/decorators.js';\nimport { vannaDesignTokens } from '../styles/vanna-design-tokens.js';\n\nexport interface TaskItem {\n  id: string;\n  title: string;\n  description?: string;\n  status: 'pending' | 'running' | 'completed' | 'failed';\n  progress?: number;\n  timestamp?: string;\n}\n\n@customElement('rich-task-list')\nexport class RichTaskList extends LitElement {\n  static styles = [\n    vannaDesignTokens,\n    css`\n      :host {\n        display: block;\n        margin-bottom: var(--vanna-space-4);\n        font-family: var(--vanna-font-family-default);\n      }\n\n      .task-list {\n        border: 1px solid var(--vanna-outline-default);\n        border-radius: var(--vanna-border-radius-lg);\n        background: var(--vanna-background-default);\n        box-shadow: var(--vanna-shadow-sm);\n        overflow: hidden;\n        transition: box-shadow var(--vanna-duration-200) ease;\n      }\n\n      .task-list:hover {\n        box-shadow: var(--vanna-shadow-md);\n      }\n\n      .task-list-header {\n        padding: var(--vanna-space-4) var(--vanna-space-5);\n        background: var(--vanna-background-higher);\n        border-bottom: 1px solid var(--vanna-outline-default);\n      }\n\n      .task-list-title {\n        margin: 0 0 var(--vanna-space-3) 0;\n        font-size: 1rem;\n        font-weight: 600;\n        color: var(--vanna-foreground-default);\n      }\n\n      .task-list-progress {\n        display: flex;\n        align-items: center;\n        gap: var(--vanna-space-3);\n      }\n\n      .progress-text {\n        font-size: 0.875rem;\n        color: var(--vanna-foreground-dimmer);\n        min-width: fit-content;\n      }\n\n      .progress-bar {\n        flex: 1;\n        height: 6px;\n        background: var(--vanna-background-root);\n        border-radius: 3px;\n        overflow: hidden;\n      }\n\n      .progress-fill {\n        height: 100%;\n        background: var(--vanna-accent-primary-default);\n        border-radius: 3px;\n        transition: width var(--vanna-duration-300) ease;\n      }\n\n      .progress-fill.animated {\n        animation: progressPulse 2s ease-in-out infinite;\n      }\n\n      @keyframes progressPulse {\n        0%, 100% { opacity: 1; }\n        50% { opacity: 0.7; }\n      }\n\n      .progress-fill.status-success {\n        background: var(--vanna-accent-positive-default);\n      }\n\n      .progress-fill.status-warning {\n        background: var(--vanna-accent-warning-default);\n      }\n\n      .progress-fill.status-error {\n        background: var(--vanna-accent-negative-default);\n      }\n\n      .task-list-items {\n        padding: var(--vanna-space-2);\n      }\n\n      .task-item {\n        display: flex;\n        align-items: flex-start;\n        gap: var(--vanna-space-3);\n        padding: var(--vanna-space-3);\n        border-radius: var(--vanna-border-radius-md);\n        transition: background-color var(--vanna-duration-200) ease;\n      }\n\n      .task-item:hover {\n        background: var(--vanna-background-root);\n      }\n\n      .task-item.status-completed {\n        opacity: 0.7;\n      }\n\n      .task-item.status-failed {\n        background: rgba(239, 68, 68, 0.1);\n      }\n\n      .task-icon {\n        font-size: 1rem;\n        margin-top: 0.125rem;\n      }\n\n      .task-content {\n        flex: 1;\n        min-width: 0;\n      }\n\n      .task-title {\n        font-weight: 500;\n        color: var(--vanna-foreground-default);\n        margin-bottom: var(--vanna-space-1);\n      }\n\n      .task-description {\n        font-size: 0.875rem;\n        color: var(--vanna-foreground-dimmer);\n        margin-bottom: var(--vanna-space-2);\n      }\n\n      .task-progress {\n        display: flex;\n        align-items: center;\n        gap: var(--vanna-space-2);\n        margin-bottom: var(--vanna-space-2);\n      }\n\n      .task-progress-bar {\n        flex: 1;\n        height: 4px;\n        background: var(--vanna-background-root);\n        border-radius: 2px;\n        overflow: hidden;\n      }\n\n      .task-progress-fill {\n        height: 100%;\n        background: var(--vanna-accent-primary-default);\n        border-radius: 2px;\n        transition: width var(--vanna-duration-300) ease;\n      }\n\n      .task-progress-text {\n        font-size: 0.75rem;\n        color: var(--vanna-foreground-dimmer);\n        min-width: fit-content;\n      }\n\n      .task-timestamp {\n        font-size: 0.75rem;\n        color: var(--vanna-foreground-dimmest);\n      }\n\n      /* Responsive adjustments */\n      @media (max-width: 768px) {\n        .task-list-header {\n          padding-left: var(--vanna-space-4);\n          padding-right: var(--vanna-space-4);\n        }\n\n        .task-list-progress {\n          flex-direction: column;\n          align-items: stretch;\n          gap: var(--vanna-space-2);\n        }\n      }\n    `\n  ];\n\n  @property() title = '';\n  @property({ type: Array }) tasks: TaskItem[] = [];\n  @property({ type: Boolean }) showProgress = true;\n  @property({ type: Boolean }) showTimestamps = false;\n  @property() theme: 'light' | 'dark' = 'dark';\n\n  private get completedTasks(): number {\n    return this.tasks.filter(task => task.status === 'completed').length;\n  }\n\n  private get progressPercentage(): number {\n    return this.tasks.length > 0 ? (this.completedTasks / this.tasks.length) * 100 : 0;\n  }\n\n  private getStatusIcon(status: string): string {\n    const icons = {\n      'pending': '⏳',\n      'running': '🔄',\n      'completed': '✅',\n      'failed': '❌'\n    };\n    return icons[status as keyof typeof icons] || '⏳';\n  }\n\n  private renderTask(task: TaskItem) {\n    const statusIcon = this.getStatusIcon(task.status);\n\n    return html`\n      <div class=\"task-item status-${task.status}\" data-task-id=\"${task.id}\">\n        <div class=\"task-icon\">${statusIcon}</div>\n        <div class=\"task-content\">\n          <div class=\"task-title\">${task.title}</div>\n          ${task.description ? html`\n            <div class=\"task-description\">${task.description}</div>\n          ` : ''}\n          ${task.progress !== null && task.progress !== undefined ? html`\n            <div class=\"task-progress\">\n              <div class=\"task-progress-bar\">\n                <div class=\"task-progress-fill\" style=\"width: ${task.progress * 100}%\"></div>\n              </div>\n              <span class=\"task-progress-text\">${Math.round(task.progress * 100)}%</span>\n            </div>\n          ` : ''}\n          ${this.showTimestamps && task.timestamp ? html`\n            <div class=\"task-timestamp\">${task.timestamp}</div>\n          ` : ''}\n        </div>\n      </div>\n    `;\n  }\n\n  render() {\n    return html`\n      <div class=\"task-list\">\n        <div class=\"task-list-header\">\n          <h3 class=\"task-list-title\">${this.title}</h3>\n          ${this.showProgress ? html`\n            <div class=\"task-list-progress\">\n              <span class=\"progress-text\">${this.completedTasks}/${this.tasks.length} completed</span>\n              <div class=\"progress-bar\">\n                <div class=\"progress-fill\" style=\"width: ${this.progressPercentage}%\"></div>\n              </div>\n            </div>\n          ` : ''}\n        </div>\n        <div class=\"task-list-items\">\n          ${this.tasks.map(task => this.renderTask(task))}\n        </div>\n      </div>\n    `;\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'rich-task-list': RichTaskList;\n  }\n}"
  },
  {
    "path": "frontends/webcomponent/src/components/vanna-chat.stories.ts",
    "content": "import type { Meta, StoryObj } from '@storybook/web-components';\nimport { html } from 'lit';\nimport './vanna-chat';\nimport './vanna-message';\nimport './plotly-chart';\n\nconst meta: Meta = {\n  title: 'Components/VannaChat',\n  component: 'vanna-chat',\n  parameters: {\n    layout: 'fullscreen',\n    backgrounds: {\n      default: 'light',\n      values: [\n        { name: 'light', value: '#f5f7fa' },\n        { name: 'dark', value: 'rgb(11, 15, 25)' },\n      ],\n    },\n  },\n  argTypes: {\n    title: { control: 'text' },\n    placeholder: { control: 'text' },\n    disabled: { control: 'boolean' },\n    showProgress: { control: 'boolean' },\n    maxAutonomy: { control: 'boolean' },\n    theme: {\n      control: 'select',\n      options: ['dark', 'light'],\n      description: 'Theme variant'\n    },\n  },\n};\n\nexport default meta;\ntype Story = StoryObj;\n\nexport const Default: Story = {\n  args: {\n    title: 'Vanna AI Agent',\n    placeholder: 'Describe what you want to build...',\n    disabled: false,\n    showProgress: true,\n    maxAutonomy: false,\n    theme: 'light',\n  },\n  render: (args) => html`\n    <div style=\"height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; background: ${args.theme === 'light' ? '#f5f7fa' : 'rgb(11, 15, 25)'};\">\n      <vanna-chat\n        .title=${args.title}\n        .placeholder=${args.placeholder}\n        .disabled=${args.disabled}\n        .showProgress=${args.showProgress}\n        .maxAutonomy=${args.maxAutonomy}\n        theme=${args.theme}>\n      </vanna-chat>\n    </div>\n  `,\n};\n\nexport const LightMode: Story = {\n  args: {\n    title: 'Vanna AI Agent',\n    placeholder: 'Describe what you want to build...',\n    disabled: false,\n    showProgress: true,\n    maxAutonomy: false,\n    theme: 'light',\n  },\n  render: (args) => html`\n    <div style=\"height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; background: ${args.theme === 'light' ? '#f5f7fa' : 'rgb(11, 15, 25)'};\">\n      <vanna-chat\n        .title=${args.title}\n        .placeholder=${args.placeholder}\n        .disabled=${args.disabled}\n        .showProgress=${args.showProgress}\n        .maxAutonomy=${args.maxAutonomy}\n        theme=${args.theme}>\n      </vanna-chat>\n    </div>\n  `,\n};\n\nexport const WithConversation: Story = {\n  args: {\n    title: 'Vanna AI Agent',\n    placeholder: 'Continue the conversation...',\n    disabled: false,\n    showProgress: true,\n    maxAutonomy: true,\n    theme: 'light',\n  },\n  render: (args) => {\n    setTimeout(() => {\n      const chat = document.querySelector('vanna-chat') as any;\n      const tracker = chat?.getProgressTracker();\n\n      if (chat && tracker) {\n        // Add conversation messages\n        chat.addMessage('Create a dashboard for analyzing customer data', 'user');\n        chat.addMessage('I\\'ll help you create a customer data dashboard. Let me break this down into steps and get started.', 'assistant');\n        chat.addMessage('What specific metrics would you like to track? Revenue, acquisition, retention, or something else?', 'assistant');\n        chat.addMessage('Focus on revenue and customer acquisition metrics', 'user');\n\n        // Add progress items\n        const id1 = tracker.addItem('Analyze requirements', 'Understanding dashboard needs');\n        const id2 = tracker.addItem('Design data schema', 'Planning database structure');\n        tracker.addItem('Create visualization components', 'Building charts and graphs');\n        tracker.addItem('Implement filtering', 'Adding date range and segment filters');\n\n        // Update progress states\n        tracker.updateItem(id1, 'completed');\n        tracker.updateItem(id2, 'in_progress', 'Identifying key metrics and data sources');\n\n        // Set status\n        chat.setStatus('working', 'Analyzing data requirements...', 'Step 2 of 4');\n      }\n    }, 100);\n\n    return html`\n      <div style=\"height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; background: ${args.theme === 'light' ? '#f5f7fa' : 'rgb(11, 15, 25)'};\">\n        <vanna-chat\n          .title=${args.title}\n          .placeholder=${args.placeholder}\n          .disabled=${args.disabled}\n          .showProgress=${args.showProgress}\n          .maxAutonomy=${args.maxAutonomy}\n          theme=${args.theme}>\n        </vanna-chat>\n      </div>\n    `;\n  },\n};\n\nexport const MaxAutonomyMode: Story = {\n  args: {\n    title: 'Vanna AI Agent - Max Autonomy',\n    placeholder: 'Describe your project...',\n    disabled: false,\n    showProgress: true,\n    maxAutonomy: true,\n    theme: 'light',\n  },\n  render: (args) => {\n    setTimeout(() => {\n      const chat = document.querySelector('vanna-chat') as any;\n      const tracker = chat?.getProgressTracker();\n\n      if (chat && tracker) {\n        chat.addMessage('Build a full-stack e-commerce app with user authentication, product catalog, shopping cart, and payment processing', 'user');\n        chat.addMessage('Perfect! I\\'ll build a complete e-commerce application for you. Since Max Autonomy is enabled, I\\'ll handle all the technical decisions and implementation details automatically.', 'assistant');\n\n        // Comprehensive task list for full autonomy\n        const tasks = [\n          'Set up project structure',\n          'Configure development environment',\n          'Design database schema',\n          'Implement user authentication',\n          'Build product catalog API',\n          'Create shopping cart functionality',\n          'Integrate payment processing',\n          'Develop frontend components',\n          'Add responsive design',\n          'Implement search & filtering',\n          'Set up testing framework',\n          'Configure deployment pipeline'\n        ];\n\n        tasks.forEach((task, index) => {\n          const id = tracker.addItem(task, `Feature ${index + 1} of ${tasks.length}`);\n          if (index < 3) tracker.updateItem(id, 'completed');\n          else if (index === 3) tracker.updateItem(id, 'in_progress', 'Setting up JWT tokens and password hashing');\n        });\n\n        chat.setStatus('working', 'Building authentication system...', 'High autonomy mode active');\n      }\n    }, 100);\n\n    return html`\n      <div style=\"height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; background: ${args.theme === 'light' ? '#f5f7fa' : 'rgb(11, 15, 25)'};\">\n        <vanna-chat\n          .title=${args.title}\n          .placeholder=${args.placeholder}\n          .disabled=${args.disabled}\n          .showProgress=${args.showProgress}\n          .maxAutonomy=${args.maxAutonomy}\n          theme=${args.theme}>\n        </vanna-chat>\n      </div>\n    `;\n  },\n};\n\nexport const WorkingState: Story = {\n  args: {\n    title: 'Vanna AI Agent',\n    placeholder: 'Ask me anything...',\n    disabled: true,\n    showProgress: true,\n    maxAutonomy: false,\n    theme: 'light',\n  },\n  render: (args) => {\n    setTimeout(() => {\n      const chat = document.querySelector('vanna-chat') as any;\n      const tracker = chat?.getProgressTracker();\n\n      if (chat && tracker) {\n        chat.addMessage('Generate a monthly sales report with charts', 'user');\n\n        const id1 = tracker.addItem('Connect to database', 'Establishing secure connection');\n        const id2 = tracker.addItem('Query sales data', 'Fetching monthly records');\n        tracker.addItem('Process data', 'Calculating totals and trends');\n        tracker.addItem('Generate charts', 'Creating visualizations');\n        tracker.addItem('Format report', 'Compiling final document');\n\n        tracker.updateItem(id1, 'completed');\n        tracker.updateItem(id2, 'in_progress', 'SELECT * FROM sales WHERE date >= 2024-01...');\n\n        chat.setStatus('working', 'Querying sales database...', 'Processing 12,543 records');\n      }\n    }, 100);\n\n    return html`\n      <div style=\"height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; background: ${args.theme === 'light' ? '#f5f7fa' : 'rgb(11, 15, 25)'};\">\n        <vanna-chat\n          .title=${args.title}\n          .placeholder=${args.placeholder}\n          .disabled=${args.disabled}\n          .showProgress=${args.showProgress}\n          .maxAutonomy=${args.maxAutonomy}\n          theme=${args.theme}>\n        </vanna-chat>\n      </div>\n    `;\n  },\n};\n\nexport const CompactMode: Story = {\n  args: {\n    title: 'Vanna AI Agent',\n    placeholder: 'Quick question...',\n    disabled: false,\n    showProgress: false,\n    maxAutonomy: false,\n    theme: 'light',\n  },\n  render: (args) => {\n    setTimeout(() => {\n      const chat = document.querySelector('vanna-chat') as any;\n      if (chat) {\n        chat.addMessage('What\\'s the average order value this month?', 'user');\n        chat.addMessage('Let me query that for you...', 'assistant');\n        chat.setStatus('working', 'Calculating average order value...', '2.1s');\n      }\n    }, 100);\n\n    return html`\n      <div style=\"height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; background: ${args.theme === 'light' ? '#f5f7fa' : 'rgb(11, 15, 25)'};\">\n        <vanna-chat\n          .title=${args.title}\n          .placeholder=${args.placeholder}\n          .disabled=${args.disabled}\n          .showProgress=${args.showProgress}\n          .maxAutonomy=${args.maxAutonomy}\n          theme=${args.theme}>\n        </vanna-chat>\n      </div>\n    `;\n  },\n};\n\nexport const WithRichComponents: Story = {\n  args: {\n    title: 'Vanna AI Agent - Rich Components',\n    placeholder: 'Ask me to analyze data or build something...',\n    disabled: false,\n    showProgress: true,\n    maxAutonomy: false,\n    theme: 'light',\n  },\n  render: (args) => {\n    setTimeout(() => {\n      const chat = document.querySelector('vanna-chat') as any;\n      const tracker = chat?.getProgressTracker();\n\n      if (chat && tracker) {\n        // Initial conversation\n        chat.addMessage('Create a comprehensive sales dashboard with multiple visualizations and export capabilities', 'user');\n        chat.addMessage('I\\'ll create a comprehensive sales dashboard for you. Let me break this down into clear tasks and show you the progress with rich components.', 'assistant');\n\n        // Add progress tasks\n        const taskId1 = tracker.addItem('Analyze requirements', 'Understanding dashboard specifications');\n        const taskId2 = tracker.addItem('Design data schema', 'Planning database structure');\n        const taskId3 = tracker.addItem('Create visualizations', 'Building charts and graphs');\n        const taskId4 = tracker.addItem('Add export features', 'Implementing PDF and Excel export');\n        tracker.addItem('Deploy dashboard', 'Setting up production environment');\n\n        tracker.updateItem(taskId1, 'completed');\n        tracker.updateItem(taskId2, 'completed');\n        tracker.updateItem(taskId3, 'in_progress', 'Creating revenue trend charts...');\n\n        chat.setStatus('working', 'Building visualization components...', 'Step 3 of 5');\n\n        // Add rich components after a delay\n        setTimeout(() => {\n          const componentManager = chat.componentManager;\n          if (!componentManager) return;\n\n          // Add info notification\n          componentManager.processUpdate({\n            operation: 'create',\n            target_id: 'info-notification',\n            component: {\n              id: 'info-notification',\n              type: 'notification',\n              data: {\n                title: 'Dashboard Progress',\n                message: 'Your sales dashboard is being built with the following components: revenue trends, customer analytics, and performance metrics.',\n                level: 'info',\n                dismissible: true,\n                actions: []\n              },\n              layout: { position: 'append', size: {}, z_index: 0, classes: [] },\n              theme: {},\n              lifecycle: 'create'\n            },\n            timestamp: new Date().toISOString()\n          });\n\n          // Add status indicator\n          componentManager.processUpdate({\n            operation: 'create',\n            target_id: 'status-chart-generation',\n            component: {\n              id: 'status-chart-generation',\n              type: 'status_indicator',\n              data: {\n                status: 'loading',\n                message: 'Generating revenue trend charts...',\n                pulse: true\n              },\n              layout: { position: 'append', size: {}, z_index: 0, classes: [] },\n              theme: {},\n              lifecycle: 'create'\n            },\n            timestamp: new Date().toISOString()\n          });\n\n          // Add progress bar for chart generation\n          componentManager.processUpdate({\n            operation: 'create',\n            target_id: 'chart-progress',\n            component: {\n              id: 'chart-progress',\n              type: 'progress_bar',\n              data: {\n                progress: 65,\n                status: 'active',\n                label: 'Chart Generation Progress',\n                detail: 'Processing 12,543 sales records...',\n                animated: true\n              },\n              layout: { position: 'append', size: {}, z_index: 0, classes: [] },\n              theme: {},\n              lifecycle: 'create'\n            },\n            timestamp: new Date().toISOString()\n          });\n\n          // Add task list card\n          componentManager.processUpdate({\n            operation: 'create',\n            target_id: 'dashboard-tasks',\n            component: {\n              id: 'dashboard-tasks',\n              type: 'task_list',\n              data: {\n                title: 'Dashboard Components',\n                tasks: [\n                  {\n                    id: 'task-1',\n                    title: 'Revenue Trend Chart',\n                    description: 'Monthly revenue tracking with year-over-year comparison',\n                    status: 'completed',\n                    progress: 100,\n                    timestamp: '2024-01-15 14:32:00'\n                  },\n                  {\n                    id: 'task-2',\n                    title: 'Customer Acquisition Funnel',\n                    description: 'Lead to customer conversion visualization',\n                    status: 'running',\n                    progress: 75,\n                    timestamp: '2024-01-15 14:45:00'\n                  },\n                  {\n                    id: 'task-3',\n                    title: 'Geographic Sales Map',\n                    description: 'Interactive map showing sales by region',\n                    status: 'pending',\n                    progress: 0,\n                    timestamp: null\n                  },\n                  {\n                    id: 'task-4',\n                    title: 'Performance Metrics KPIs',\n                    description: 'Key performance indicators dashboard',\n                    status: 'pending',\n                    progress: 0,\n                    timestamp: null\n                  }\n                ],\n                progress: 58\n              },\n              layout: { position: 'append', size: {}, z_index: 0, classes: [] },\n              theme: {},\n              lifecycle: 'create'\n            },\n            timestamp: new Date().toISOString()\n          });\n\n          // Add data summary card\n          componentManager.processUpdate({\n            operation: 'create',\n            target_id: 'data-summary',\n            component: {\n              id: 'data-summary',\n              type: 'card',\n              data: {\n                title: 'Data Analysis Summary',\n                subtitle: 'Sales Data Processing Results',\n                content: 'Successfully processed 12,543 sales records from the last 12 months. Found key trends in customer behavior and revenue patterns.',\n                icon: '📊',\n                status: 'success',\n                collapsible: true,\n                actions: [\n                  { label: 'View Details', action: 'view-details', variant: 'primary' },\n                  { label: 'Export Data', action: 'export', variant: 'secondary' }\n                ]\n              },\n              layout: { position: 'append', size: {}, z_index: 0, classes: [] },\n              theme: {},\n              lifecycle: 'create'\n            },\n            timestamp: new Date().toISOString()\n          });\n\n          // Add markdown text with insights\n          componentManager.processUpdate({\n            operation: 'create',\n            target_id: 'insights-text',\n            component: {\n              id: 'insights-text',\n              type: 'text',\n              data: {\n                content: `# Dashboard Insights\\n\\nBased on the data analysis, here are the key findings:\\n\\n## Revenue Trends\\n- **23% increase** in Q4 sales compared to Q3\\n- Peak sales month: **December** ($1.2M)\\n- Lowest performing month: **February** ($680K)\\n\\n## Customer Behavior\\n- Average order value: **$156.78**\\n- Customer retention rate: **89.3%**\\n- Most popular product category: **Electronics**\\n\\n## Recommendations\\n1. **Focus marketing efforts** on February to boost sales\\n2. **Expand electronics inventory** for peak seasons\\n3. **Implement loyalty program** to maintain high retention\\n\\n*Dashboard generation is 65% complete. Estimated completion: 3-4 minutes.*`,\n                markdown: true\n              },\n              layout: { position: 'append', size: {}, z_index: 0, classes: [] },\n              theme: {},\n              lifecycle: 'create'\n            },\n            timestamp: new Date().toISOString()\n          });\n\n        }, 1500);\n\n        // Update components after more time\n        setTimeout(() => {\n          const componentManager = chat.componentManager;\n          if (!componentManager) return;\n\n          // Update status indicator to success\n          componentManager.processUpdate({\n            operation: 'update',\n            target_id: 'status-chart-generation',\n            updates: {\n              status: 'success',\n              message: 'Revenue charts generated successfully',\n              pulse: false\n            },\n            timestamp: new Date().toISOString()\n          });\n\n          // Update progress bar\n          componentManager.processUpdate({\n            operation: 'update',\n            target_id: 'chart-progress',\n            updates: {\n              progress: 100,\n              status: 'success',\n              detail: 'All charts generated successfully!',\n              animated: false\n            },\n            timestamp: new Date().toISOString()\n          });\n\n          // Add success notification\n          componentManager.processUpdate({\n            operation: 'create',\n            target_id: 'success-notification',\n            component: {\n              id: 'success-notification',\n              type: 'notification',\n              data: {\n                title: 'Charts Ready!',\n                message: 'Your revenue trend charts have been generated and are ready for review.',\n                level: 'success',\n                dismissible: true,\n                actions: [\n                  { label: 'View Charts', action: 'view-charts', variant: 'primary' },\n                  { label: 'Continue', action: 'continue', variant: 'secondary' }\n                ]\n              },\n              layout: { position: 'append', size: {}, z_index: 0, classes: [] },\n              theme: {},\n              lifecycle: 'create'\n            },\n            timestamp: new Date().toISOString()\n          });\n\n          // Update progress tracker\n          tracker.updateItem(taskId3, 'completed');\n          tracker.updateItem(taskId4, 'in_progress', 'Adding PDF export functionality...');\n          chat.setStatus('working', 'Adding export capabilities...', 'Step 4 of 5');\n\n        }, 4000);\n      }\n    }, 100);\n\n    return html`\n      <div style=\"height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; background: ${args.theme === 'light' ? '#f5f7fa' : 'rgb(11, 15, 25)'};\">\n        <vanna-chat\n          .title=${args.title}\n          .placeholder=${args.placeholder}\n          .disabled=${args.disabled}\n          .showProgress=${args.showProgress}\n          .maxAutonomy=${args.maxAutonomy}\n          theme=${args.theme}>\n        </vanna-chat>\n      </div>\n    `;\n  },\n};\n\nexport const WithToolExecutionComponents: Story = {\n  args: {\n    title: 'Vanna AI Agent - Tool Execution',\n    placeholder: 'Ask me to run commands or execute tools...',\n    disabled: false,\n    showProgress: true,\n    maxAutonomy: true,\n    theme: 'light',\n  },\n  render: (args) => {\n    setTimeout(() => {\n      const chat = document.querySelector('vanna-chat') as any;\n      const tracker = chat?.getProgressTracker();\n\n      if (chat && tracker) {\n        // Initial conversation about running tools\n        chat.addMessage('Run a data analysis script and deploy the results to production', 'user');\n        chat.addMessage('I\\'ll execute the data analysis script and handle the deployment. Let me run the necessary tools and show you the execution details.', 'assistant');\n\n        // Add progress tasks\n        const taskId1 = tracker.addItem('Run data analysis script', 'Executing Python analysis tools');\n        const taskId2 = tracker.addItem('Process results', 'Formatting and validating output');\n        const taskId3 = tracker.addItem('Deploy to production', 'Uploading to production server');\n\n        tracker.updateItem(taskId1, 'in_progress', 'Running analysis.py...');\n\n        chat.setStatus('working', 'Executing data analysis tools...', 'Max autonomy enabled');\n\n        // Add tool execution components after a delay\n        setTimeout(() => {\n          const componentManager = chat.componentManager;\n          if (!componentManager) return;\n\n          // Add tool execution component for data analysis\n          componentManager.processUpdate({\n            operation: 'create',\n            target_id: 'tool-analysis',\n            component: {\n              id: 'tool-analysis',\n              type: 'tool_execution',\n              data: {\n                tool_name: 'Python Script Executor',\n                status: 'running',\n                progress: 45,\n                duration: '2.3s',\n                arguments: {\n                  script: 'analysis.py',\n                  dataset: 'sales_data_2024.csv',\n                  output_format: 'json',\n                  verbose: true\n                },\n                result: null,\n                error: null,\n                logs: [\n                  { timestamp: '14:32:01', level: 'INFO', message: 'Loading dataset: sales_data_2024.csv' },\n                  { timestamp: '14:32:02', level: 'INFO', message: 'Found 12,543 records' },\n                  { timestamp: '14:32:03', level: 'INFO', message: 'Running correlation analysis...' },\n                  { timestamp: '14:32:04', level: 'INFO', message: 'Processing revenue trends...' }\n                ]\n              },\n              layout: { position: 'append', size: {}, z_index: 0, classes: [] },\n              theme: {},\n              lifecycle: 'create'\n            },\n            timestamp: new Date().toISOString()\n          });\n\n          // Add warning notification about data processing\n          componentManager.processUpdate({\n            operation: 'create',\n            target_id: 'warning-notification',\n            component: {\n              id: 'warning-notification',\n              type: 'notification',\n              data: {\n                title: 'Large Dataset Detected',\n                message: 'Processing 12,543 records. This may take a few minutes to complete.',\n                level: 'warning',\n                dismissible: true,\n                actions: [\n                  { label: 'Monitor Progress', action: 'monitor', variant: 'primary' }\n                ]\n              },\n              layout: { position: 'append', size: {}, z_index: 0, classes: [] },\n              theme: {},\n              lifecycle: 'create'\n            },\n            timestamp: new Date().toISOString()\n          });\n\n        }, 1500);\n\n        // Complete first tool and start second\n        setTimeout(() => {\n          const componentManager = chat.componentManager;\n          if (!componentManager) return;\n\n          // Update first tool to completed\n          componentManager.processUpdate({\n            operation: 'update',\n            target_id: 'tool-analysis',\n            updates: {\n              status: 'completed',\n              progress: 100,\n              duration: '4.7s',\n              result: JSON.stringify({\n                total_revenue: 1847259.32,\n                avg_order_value: 156.78,\n                top_product: 'Electronics',\n                growth_rate: 0.23,\n                recommendations: ['Expand Q4 marketing', 'Focus on electronics inventory']\n              }, null, 2),\n              logs: [\n                { timestamp: '14:32:01', level: 'INFO', message: 'Loading dataset: sales_data_2024.csv' },\n                { timestamp: '14:32:02', level: 'INFO', message: 'Found 12,543 records' },\n                { timestamp: '14:32:03', level: 'INFO', message: 'Running correlation analysis...' },\n                { timestamp: '14:32:04', level: 'INFO', message: 'Processing revenue trends...' },\n                { timestamp: '14:32:06', level: 'INFO', message: 'Analysis complete! Generated insights.' },\n                { timestamp: '14:32:06', level: 'INFO', message: 'Output saved to results.json' }\n              ]\n            },\n            timestamp: new Date().toISOString()\n          });\n\n          // Update progress tracker\n          tracker.updateItem(taskId1, 'completed');\n          tracker.updateItem(taskId2, 'in_progress', 'Validating analysis results...');\n\n          // Add second tool execution for deployment\n          componentManager.processUpdate({\n            operation: 'create',\n            target_id: 'tool-deploy',\n            component: {\n              id: 'tool-deploy',\n              type: 'tool_execution',\n              data: {\n                tool_name: 'Production Deployer',\n                status: 'running',\n                progress: 20,\n                duration: '1.2s',\n                arguments: {\n                  source: 'results.json',\n                  target: 'prod-server-01',\n                  backup: true,\n                  validate: true\n                },\n                result: null,\n                error: null,\n                logs: [\n                  { timestamp: '14:32:08', level: 'INFO', message: 'Connecting to prod-server-01...' },\n                  { timestamp: '14:32:09', level: 'INFO', message: 'Creating backup of existing data...' }\n                ]\n              },\n              layout: { position: 'append', size: {}, z_index: 0, classes: [] },\n              theme: {},\n              lifecycle: 'create'\n            },\n            timestamp: new Date().toISOString()\n          });\n\n        }, 4000);\n\n        // Complete deployment\n        setTimeout(() => {\n          const componentManager = chat.componentManager;\n          if (!componentManager) return;\n\n          // Complete second tool\n          componentManager.processUpdate({\n            operation: 'update',\n            target_id: 'tool-deploy',\n            updates: {\n              status: 'completed',\n              progress: 100,\n              duration: '6.1s',\n              result: 'Deployment successful! Results available at: https://dashboard.company.com/sales-analysis',\n              logs: [\n                { timestamp: '14:32:08', level: 'INFO', message: 'Connecting to prod-server-01...' },\n                { timestamp: '14:32:09', level: 'INFO', message: 'Creating backup of existing data...' },\n                { timestamp: '14:32:11', level: 'INFO', message: 'Uploading results.json...' },\n                { timestamp: '14:32:13', level: 'INFO', message: 'Validating deployment...' },\n                { timestamp: '14:32:14', level: 'INFO', message: 'Deployment successful!' }\n              ]\n            },\n            timestamp: new Date().toISOString()\n          });\n\n          // Update progress tracker\n          tracker.updateItem(taskId2, 'completed');\n          tracker.updateItem(taskId3, 'completed');\n\n          // Add success notification\n          componentManager.processUpdate({\n            operation: 'create',\n            target_id: 'deploy-success',\n            component: {\n              id: 'deploy-success',\n              type: 'notification',\n              data: {\n                title: 'Deployment Complete!',\n                message: 'Data analysis results have been successfully deployed to production. Dashboard is now live.',\n                level: 'success',\n                dismissible: true,\n                actions: [\n                  { label: 'View Dashboard', action: 'view-dashboard', variant: 'primary' },\n                  { label: 'Download Report', action: 'download', variant: 'secondary' }\n                ]\n              },\n              layout: { position: 'append', size: {}, z_index: 0, classes: [] },\n              theme: {},\n              lifecycle: 'create'\n            },\n            timestamp: new Date().toISOString()\n          });\n\n          // Add final status indicator\n          componentManager.processUpdate({\n            operation: 'create',\n            target_id: 'final-status',\n            component: {\n              id: 'final-status',\n              type: 'status_indicator',\n              data: {\n                status: 'success',\n                message: 'All tools executed successfully',\n                pulse: false\n              },\n              layout: { position: 'append', size: {}, z_index: 0, classes: [] },\n              theme: {},\n              lifecycle: 'create'\n            },\n            timestamp: new Date().toISOString()\n          });\n\n          // Update chat status\n          chat.setStatus('idle', 'All tasks completed successfully', 'Ready for next request');\n\n        }, 7000);\n      }\n    }, 100);\n\n    return html`\n      <div style=\"height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; background: ${args.theme === 'light' ? '#f5f7fa' : 'rgb(11, 15, 25)'};\">\n        <vanna-chat\n          .title=${args.title}\n          .placeholder=${args.placeholder}\n          .disabled=${args.disabled}\n          .showProgress=${args.showProgress}\n          .maxAutonomy=${args.maxAutonomy}\n          theme=${args.theme}>\n        </vanna-chat>\n      </div>\n    `;\n  },\n};\n\nexport const WithChart: Story = {\n  args: {\n    title: 'Vanna AI Agent - Chart Display',\n    placeholder: 'Ask me to analyze data...',\n    disabled: false,\n    showProgress: true,\n    maxAutonomy: false,\n    theme: 'light',\n  },\n  render: (args) => {\n    setTimeout(() => {\n      const chat = document.querySelector('vanna-chat') as any;\n      const tracker = chat?.getProgressTracker();\n\n      if (chat && tracker) {\n        // Initial conversation\n        chat.addMessage('Show me the top 10 artists by sales', 'user');\n        chat.addMessage('I\\'ll analyze the sales data and create a visualization for you.', 'assistant');\n\n        // Add progress tasks\n        const taskId1 = tracker.addItem('Query sales database', 'Fetching artist sales data');\n        const taskId2 = tracker.addItem('Process results', 'Calculating total sales per artist');\n        const taskId3 = tracker.addItem('Create visualization', 'Generating bar chart');\n\n        tracker.updateItem(taskId1, 'completed');\n        tracker.updateItem(taskId2, 'completed');\n        tracker.updateItem(taskId3, 'in_progress', 'Rendering Plotly chart...');\n\n        chat.setStatus('working', 'Creating visualization...', 'Step 3 of 3');\n\n        // Add chart component after a delay\n        setTimeout(() => {\n          const componentManager = chat.componentManager;\n          if (!componentManager) return;\n\n          // Add the chart component with the data you provided\n          componentManager.processUpdate({\n            operation: 'create',\n            target_id: '8e275121-e3f4-4b99-87f2-fabc3ef66216',\n            component: {\n              id: '8e275121-e3f4-4b99-87f2-fabc3ef66216',\n              type: 'chart',\n              data: {\n                data: [\n                  {\n                    hovertemplate: 'artist=%{x}<br>total_sales=%{y}<extra></extra>',\n                    legendgroup: '',\n                    marker: {\n                      color: '#636efa',\n                      pattern: {\n                        shape: ''\n                      }\n                    },\n                    name: '',\n                    orientation: 'v',\n                    showlegend: false,\n                    textposition: 'auto',\n                    x: [\n                      'Deep Purple',\n                      'Eric Clapton',\n                      'Faith No More',\n                      'Iron Maiden',\n                      'Led Zeppelin',\n                      'Lost',\n                      'Metallica',\n                      'Os Paralamas Do Sucesso',\n                      'The Office',\n                      'U2'\n                    ],\n                    xaxis: 'x',\n                    y: [22.5, 19.8, 27.72, 138.6, 86.13, 81.59, 90.09, 41.49, 30, 105.93],\n                    yaxis: 'y',\n                    type: 'bar'\n                  }\n                ],\n                layout: {\n                  template: {\n                    data: {\n                      bar: [\n                        {\n                          error_x: {\n                            color: '#2a3f5f'\n                          },\n                          error_y: {\n                            color: '#2a3f5f'\n                          },\n                          marker: {\n                            line: {\n                              color: '#E5ECF6',\n                              width: 0.5\n                            },\n                            pattern: {\n                              fillmode: 'overlay',\n                              size: 10,\n                              solidity: 0.2\n                            }\n                          },\n                          type: 'bar'\n                        }\n                      ]\n                    },\n                    layout: {\n                      font: {\n                        color: '#2a3f5f'\n                      },\n                      xaxis: {\n                        gridcolor: 'white',\n                        linecolor: 'white',\n                        ticks: '',\n                        title: {\n                          standoff: 15\n                        },\n                        zerolinecolor: 'white',\n                        automargin: true,\n                        zerolinewidth: 2\n                      },\n                      yaxis: {\n                        gridcolor: 'white',\n                        linecolor: 'white',\n                        ticks: '',\n                        title: {\n                          standoff: 15\n                        },\n                        zerolinecolor: 'white',\n                        automargin: true,\n                        zerolinewidth: 2\n                      }\n                    }\n                  },\n                  xaxis: {\n                    anchor: 'y',\n                    domain: [0.0, 1.0],\n                    title: {\n                      text: 'artist'\n                    }\n                  },\n                  yaxis: {\n                    anchor: 'x',\n                    domain: [0.0, 1.0],\n                    title: {\n                      text: 'total_sales'\n                    }\n                  },\n                  legend: {\n                    tracegroupgap: 0\n                  },\n                  title: {\n                    text: 'Top 10 Artists by Sales'\n                  },\n                  barmode: 'relative',\n                  font: {\n                    color: '#1f2937'\n                  },\n                  paper_bgcolor: 'white',\n                  plot_bgcolor: 'white',\n                  autosize: true\n                },\n                chart_type: 'plotly',\n                title: 'Top 10 Artists by Sales',\n                width: null,\n                height: null,\n                config: {\n                  data_shape: {\n                    rows: 10,\n                    columns: 2\n                  },\n                  source_file: 'query_results_f42c1599.csv'\n                }\n              },\n              layout: { position: 'append', size: {}, z_index: 0, classes: [] },\n              theme: {},\n              lifecycle: 'create'\n            },\n            timestamp: new Date().toISOString()\n          });\n\n          // Update progress tracker\n          tracker.updateItem(taskId3, 'completed');\n          chat.setStatus('idle', 'Visualization complete', 'Ready for next query');\n\n          // Add text summary\n          componentManager.processUpdate({\n            operation: 'create',\n            target_id: 'chart-summary',\n            component: {\n              id: 'chart-summary',\n              type: 'text',\n              data: {\n                content: `Created visualization from 'query_results_f42c1599.csv' (10 rows, 2 columns).\n\n**Top Artists:**\n- Faith No More leads with 138.6 in total sales\n- U2 follows with 105.93\n- Metallica with 90.09\n\nThe chart shows the distribution of sales across the top 10 artists.`,\n                markdown: true\n              },\n              layout: { position: 'append', size: {}, z_index: 0, classes: [] },\n              theme: {},\n              lifecycle: 'create'\n            },\n            timestamp: new Date().toISOString()\n          });\n\n        }, 1500);\n      }\n    }, 100);\n\n    return html`\n      <div style=\"height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; background: ${args.theme === 'light' ? '#f5f7fa' : 'rgb(11, 15, 25)'};\">\n        <vanna-chat\n          .title=${args.title}\n          .placeholder=${args.placeholder}\n          .disabled=${args.disabled}\n          .showProgress=${args.showProgress}\n          .maxAutonomy=${args.maxAutonomy}\n          theme=${args.theme}>\n        </vanna-chat>\n      </div>\n    `;\n  },\n};\n\nexport const WithButtons: Story = {\n  args: {\n    title: 'Vanna AI Agent - Button Components',\n    placeholder: 'Click buttons to send messages...',\n    disabled: false,\n    showProgress: false,\n    maxAutonomy: false,\n    theme: 'dark',\n  },\n  render: (args) => {\n    setTimeout(() => {\n      const chat = document.querySelector('vanna-chat') as any;\n\n      if (chat) {\n        // Initial conversation\n        chat.addMessage('Show me some button options', 'user');\n        chat.addMessage('Here are some interactive buttons. Click any button to send a message with its label wrapped in square brackets.', 'assistant');\n\n        const componentManager = chat.componentManager;\n        if (!componentManager) return;\n\n        // Add a single button example\n        componentManager.processUpdate({\n          operation: 'create',\n          target_id: 'single-button-1',\n          component: {\n            id: 'single-button-1',\n            type: 'button',\n            data: {\n              label: 'Okay',\n              action: 'okay',\n              variant: 'primary',\n              size: 'medium',\n            },\n            layout: { position: 'append', size: {}, z_index: 0, classes: [] },\n            theme: {},\n            lifecycle: 'create'\n          },\n          timestamp: new Date().toISOString()\n        });\n\n        // Add button group with choices\n        componentManager.processUpdate({\n          operation: 'create',\n          target_id: 'choice-group',\n          component: {\n            id: 'choice-group',\n            type: 'button_group',\n            data: {\n              buttons: [\n                {\n                  label: 'Yes',\n                  action: 'yes',\n                  variant: 'success',\n                  icon: '✓',\n                },\n                {\n                  label: 'No',\n                  action: 'no',\n                  variant: 'error',\n                  icon: '✗',\n                },\n                {\n                  label: 'Maybe',\n                  action: 'maybe',\n                  variant: 'secondary',\n                },\n              ],\n              orientation: 'horizontal',\n              spacing: 'medium',\n              align: 'left',\n            },\n            layout: { position: 'append', size: {}, z_index: 0, classes: [] },\n            theme: {},\n            lifecycle: 'create'\n          },\n          timestamp: new Date().toISOString()\n        });\n\n        // Add more single buttons with different variants\n        const singleButtons = [\n          { label: 'Continue', variant: 'primary', icon: '→', icon_position: 'right' },\n          { label: 'Save Draft', variant: 'secondary', icon: '💾', icon_position: 'left' },\n          { label: 'Delete', variant: 'error', icon: '🗑️' },\n          { label: 'Cancel', variant: 'ghost' },\n        ];\n\n        singleButtons.forEach((btnData, index) => {\n          componentManager.processUpdate({\n            operation: 'create',\n            target_id: `single-button-${index + 2}`,\n            component: {\n              id: `single-button-${index + 2}`,\n              type: 'button',\n              data: {\n                ...btnData,\n                action: btnData.label.toLowerCase().replace(' ', '_'),\n                size: 'medium',\n              },\n              layout: { position: 'append', size: {}, z_index: 0, classes: [] },\n              theme: {},\n              lifecycle: 'create'\n            },\n            timestamp: new Date().toISOString()\n          });\n        });\n\n        // Add a vertical button group\n        componentManager.processUpdate({\n          operation: 'create',\n          target_id: 'option-group',\n          component: {\n            id: 'option-group',\n            type: 'button_group',\n            data: {\n              buttons: [\n                { label: 'Option A', action: 'option_a', variant: 'secondary' },\n                { label: 'Option B', action: 'option_b', variant: 'secondary' },\n                { label: 'Option C', action: 'option_c', variant: 'secondary' },\n              ],\n              orientation: 'vertical',\n              spacing: 'small',\n              align: 'left',\n            },\n            layout: { position: 'append', size: {}, z_index: 0, classes: [] },\n            theme: {},\n            lifecycle: 'create'\n          },\n          timestamp: new Date().toISOString()\n        });\n\n        // Listen for message-sent events to show feedback\n        chat.addEventListener('message-sent', (e: CustomEvent) => {\n          console.log('Message sent from button:', e.detail.message);\n        });\n      }\n    }, 100);\n\n    return html`\n      <div style=\"height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; background: ${args.theme === 'light' ? '#f5f7fa' : 'rgb(11, 15, 25)'};\">\n        <vanna-chat\n          .title=${args.title}\n          .placeholder=${args.placeholder}\n          .disabled=${args.disabled}\n          .showProgress=${args.showProgress}\n          .maxAutonomy=${args.maxAutonomy}\n          theme=${args.theme}>\n        </vanna-chat>\n      </div>\n    `;\n  },\n};"
  },
  {
    "path": "frontends/webcomponent/src/components/vanna-chat.ts",
    "content": "import { LitElement, html, css } from 'lit';\nimport { customElement, property, state } from 'lit/decorators.js';\nimport { vannaDesignTokens } from '../styles/vanna-design-tokens.js';\nimport { VannaApiClient, ChatStreamChunk } from '../services/api-client.js';\nimport { ComponentManager, RichComponent } from './rich-component-system.js';\nimport './vanna-status-bar.js';\nimport './vanna-progress-tracker.js';\nimport './rich-card.js';\nimport './rich-task-list.js';\nimport './rich-progress-bar.js';\nimport './plotly-chart.js';\n\n@customElement('vanna-chat')\nexport class VannaChat extends LitElement {\n  static styles = [\n    vannaDesignTokens,\n    css`\n      *, *::before, *::after {\n        box-sizing: border-box;\n      }\n\n      :host {\n        display: block;\n        font-family: var(--vanna-font-family-default);\n        --chat-primary: var(--vanna-accent-primary-default);\n        --chat-primary-stronger: var(--vanna-accent-primary-stronger);\n        --chat-primary-foreground: rgb(255, 255, 255);\n        --chat-accent-soft: var(--vanna-accent-primary-subtle);\n        --chat-outline: var(--vanna-outline-default);\n        --chat-surface: var(--vanna-background-root);\n        --chat-muted: var(--vanna-background-default);\n        --chat-muted-stronger: var(--vanna-background-higher);\n        max-width: 1024px;\n        margin: 0 auto;\n        background: var(--vanna-background-root);\n        border: 1px solid var(--vanna-outline-dimmer);\n        border-radius: var(--vanna-border-radius-2xl);\n        box-shadow: var(--vanna-shadow-xl);\n        overflow: hidden;\n        transition: box-shadow var(--vanna-duration-300) ease, transform var(--vanna-duration-300) ease;\n        position: relative;\n      }\n\n      :host(:hover) {\n        box-shadow: var(--vanna-shadow-2xl);\n        transform: translateY(-2px);\n      }\n\n      :host([theme=\"dark\"]) {\n        --chat-primary: var(--vanna-accent-primary-default);\n        --chat-primary-stronger: var(--vanna-accent-primary-stronger);\n        --chat-primary-foreground: rgb(255, 255, 255);\n        --chat-accent-soft: var(--vanna-accent-primary-subtle);\n        --chat-outline: var(--vanna-outline-default);\n        --chat-surface: var(--vanna-background-higher);\n        --chat-muted: var(--vanna-background-default);\n        --chat-muted-stronger: var(--vanna-background-highest);\n        background: var(--vanna-background-higher);\n        border-color: var(--vanna-outline-default);\n      }\n\n      :host(.maximized) {\n        position: fixed;\n        top: var(--vanna-space-6);\n        left: var(--vanna-space-6);\n        right: var(--vanna-space-6);\n        bottom: var(--vanna-space-6);\n        max-width: none;\n        width: auto;\n        margin: 0;\n        z-index: var(--vanna-z-modal);\n        border-radius: var(--vanna-border-radius-xl);\n        transform: none;\n        box-shadow: var(--vanna-shadow-2xl);\n      }\n\n      :host(.maximized):hover {\n        transform: none;\n      }\n\n      :host(.minimized) {\n        position: fixed !important;\n        bottom: var(--vanna-space-6) !important;\n        right: var(--vanna-space-6) !important;\n        width: 64px !important;\n        height: 64px !important;\n        max-width: none !important;\n        margin: 0 !important;\n        z-index: var(--vanna-z-modal) !important;\n        border-radius: var(--vanna-border-radius-full) !important;\n        cursor: pointer !important;\n        background: linear-gradient(135deg, var(--chat-primary-stronger), var(--chat-primary)) !important;\n        border: 2px solid rgba(255, 255, 255, 0.9) !important;\n        box-shadow: var(--vanna-shadow-xl) !important;\n        overflow: hidden !important;\n      }\n\n      :host(.minimized):hover {\n        transform: scale(1.05);\n        box-shadow: var(--vanna-shadow-2xl) !important;\n      }\n\n      :host(.minimized) .chat-layout {\n        display: none;\n      }\n\n      .minimized-icon {\n        display: none;\n      }\n\n      :host(.minimized) .minimized-icon {\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        width: 100%;\n        height: 100%;\n        color: var(--chat-primary-foreground);\n        font-size: 24px;\n        transition: transform var(--vanna-duration-200) ease;\n      }\n\n      :host(.minimized) .minimized-icon:hover {\n        transform: scale(1.1);\n      }\n\n      :host(.minimized) .minimized-icon svg {\n        filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));\n      }\n\n      .chat-layout {\n        display: grid;\n        grid-template-columns: minmax(0, 1fr) 300px;\n        height: 600px;\n        max-height: 80vh;\n        background: var(--chat-muted);\n      }\n\n      :host(.maximized) .chat-layout {\n        height: calc(100vh - 48px);\n        max-height: calc(100vh - 48px);\n      }\n\n      .chat-layout.compact {\n        grid-template-columns: 1fr;\n      }\n\n      .chat-main {\n        display: flex;\n        flex-direction: column;\n        border-right: 1px solid var(--chat-outline);\n        background: var(--chat-surface);\n        min-height: 0;\n      }\n\n      .chat-layout.compact .chat-main {\n        border-right: none;\n      }\n\n      .chat-header {\n        padding: var(--vanna-space-6) var(--vanna-space-7);\n        background: linear-gradient(135deg, var(--chat-primary) 0%, var(--chat-primary-stronger) 100%);\n        border-bottom: 1px solid rgba(255, 255, 255, 0.2);\n        display: flex;\n        flex-direction: column;\n        gap: var(--vanna-space-4);\n        color: var(--chat-primary-foreground);\n        position: relative;\n        overflow: hidden;\n      }\n\n      .chat-header::before {\n        content: '';\n        position: absolute;\n        top: -50%;\n        right: -50%;\n        width: 100%;\n        height: 200%;\n        background: radial-gradient(circle, rgba(255, 255, 255, 0.15) 0%, transparent 70%);\n        opacity: 0.6;\n        pointer-events: none;\n      }\n\n      :host([theme=\"dark\"]) .chat-header {\n        border-bottom-color: rgba(255, 255, 255, 0.1);\n      }\n\n      .header-top {\n        position: relative;\n        z-index: 1;\n        display: flex;\n        align-items: center;\n        gap: var(--vanna-space-4);\n        width: 100%;\n      }\n\n      .header-left {\n        display: flex;\n        align-items: center;\n        gap: var(--vanna-space-4);\n        min-width: 0;\n        flex: 1;\n      }\n\n      .header-top-actions {\n        display: inline-flex;\n        align-items: center;\n        gap: var(--vanna-space-2);\n        margin-left: auto;\n      }\n\n      .chat-avatar {\n        width: 44px;\n        height: 44px;\n        border-radius: var(--vanna-border-radius-lg);\n        background: rgba(255, 255, 255, 0.2);\n        backdrop-filter: blur(10px);\n        display: grid;\n        place-items: center;\n        font-weight: 600;\n        font-size: 16px;\n        letter-spacing: 0.02em;\n        color: var(--chat-primary-foreground);\n        border: 1px solid rgba(255, 255, 255, 0.3);\n      }\n\n      .header-text {\n        display: flex;\n        flex-direction: column;\n        gap: var(--vanna-space-1);\n        min-width: 0;\n      }\n\n      .chat-title {\n        margin: 0;\n        font-size: 18px;\n        font-weight: 600;\n        letter-spacing: -0.01em;\n        color: var(--chat-primary-foreground);\n      }\n\n      .chat-subtitle {\n        font-size: 13px;\n        letter-spacing: 0.01em;\n        opacity: 0.9;\n        font-weight: 400;\n      }\n\n      :host([theme=\"dark\"]) .chat-subtitle {\n        opacity: 0.78;\n      }\n\n      .window-controls {\n        display: inline-flex;\n        gap: var(--vanna-space-2);\n      }\n\n      .window-control-btn {\n        width: 32px;\n        height: 32px;\n        border-radius: var(--vanna-border-radius-lg);\n        border: 1px solid rgba(255, 255, 255, 0.15);\n        background: rgba(255, 255, 255, 0.1);\n        color: var(--chat-primary-foreground);\n        cursor: pointer;\n        display: inline-flex;\n        align-items: center;\n        justify-content: center;\n        transition: all var(--vanna-duration-200) ease;\n        backdrop-filter: blur(8px);\n        position: relative;\n        overflow: hidden;\n      }\n\n      .window-control-btn::before {\n        content: '';\n        position: absolute;\n        inset: 0;\n        background: linear-gradient(135deg, rgba(255, 255, 255, 0.2), transparent);\n        opacity: 0;\n        transition: opacity var(--vanna-duration-200) ease;\n      }\n\n      .window-control-btn:hover {\n        transform: translateY(-1px) scale(1.05);\n        background: rgba(255, 255, 255, 0.2);\n        box-shadow: \n          0 8px 25px -8px rgba(0, 0, 0, 0.3),\n          0 0 0 1px rgba(255, 255, 255, 0.2);\n        border-color: rgba(255, 255, 255, 0.3);\n      }\n\n      .window-control-btn:hover::before {\n        opacity: 1;\n      }\n\n      .window-control-btn:active {\n        transform: translateY(0) scale(0.95);\n      }\n\n      .window-control-btn.minimize:hover {\n        background: rgba(255, 193, 7, 0.2);\n        color: #ffc107;\n        box-shadow: \n          0 8px 25px -8px rgba(255, 193, 7, 0.4),\n          0 0 0 1px rgba(255, 193, 7, 0.3);\n      }\n\n      .window-control-btn.maximize:hover,\n      .window-control-btn.restore:hover {\n        background: rgba(40, 167, 69, 0.2);\n        color: #28a745;\n        box-shadow: \n          0 8px 25px -8px rgba(40, 167, 69, 0.4),\n          0 0 0 1px rgba(40, 167, 69, 0.3);\n      }\n\n      .window-control-btn svg {\n        width: 16px;\n        height: 16px;\n        transition: transform var(--vanna-duration-150) ease;\n      }\n\n      .window-control-btn:hover svg {\n        transform: scale(1.1);\n      }\n\n      :host([theme=\"dark\"]) .window-control-btn {\n        border-color: rgba(255, 255, 255, 0.1);\n        background: rgba(255, 255, 255, 0.05);\n      }\n\n      :host([theme=\"dark\"]) .window-control-btn:hover {\n        background: rgba(255, 255, 255, 0.15);\n        border-color: rgba(255, 255, 255, 0.25);\n      }\n\n      .chat-messages {\n        flex: 1;\n        overflow-y: auto;\n        overflow-x: hidden;\n        padding: var(--vanna-space-6) var(--vanna-space-6) var(--vanna-space-5);\n        background: linear-gradient(180deg, var(--chat-muted) 0%, var(--chat-surface) 70%);\n        scroll-behavior: smooth;\n        display: flex;\n        flex-direction: column;\n        gap: var(--vanna-space-4);\n        min-height: 0;\n        max-height: 100%;\n        position: relative;\n      }\n\n      .chat-messages::-webkit-scrollbar {\n        width: 6px;\n      }\n\n      .chat-messages::-webkit-scrollbar-track {\n        background: transparent;\n      }\n\n      .chat-messages::-webkit-scrollbar-thumb {\n        background: var(--vanna-outline-default);\n        border-radius: var(--vanna-border-radius-full);\n        border: 1px solid var(--vanna-background-root);\n      }\n\n      .chat-messages::-webkit-scrollbar-thumb:hover {\n        background: var(--vanna-outline-hover);\n      }\n\n      :host([theme=\"dark\"]) .chat-messages {\n        background: radial-gradient(circle at top, rgba(99, 102, 241, 0.12), transparent 55%), var(--chat-surface);\n      }\n\n      :host([theme=\"dark\"]) .chat-messages::-webkit-scrollbar-thumb {\n        background: var(--vanna-outline-default);\n        border-color: var(--vanna-background-higher);\n      }\n\n      /* Scroll indicator when there's content above */\n      .chat-messages::before {\n        content: '';\n        position: sticky;\n        top: 0;\n        display: block;\n        height: 1px;\n        background: linear-gradient(90deg, transparent, var(--vanna-accent-primary-default), transparent);\n        opacity: 0;\n        transition: opacity var(--vanna-duration-300) ease;\n        z-index: 10;\n        margin: 0 var(--vanna-space-4) var(--vanna-space-2);\n      }\n\n      .chat-messages.has-scroll::before {\n        opacity: 0.5;\n      }\n\n      .rich-components-container {\n        display: flex;\n        flex-direction: column;\n        gap: var(--vanna-space-4);\n      }\n\n      .rich-component-wrapper {\n        margin: var(--vanna-space-2) 0;\n        animation: fade-in-up 0.3s ease-out;\n      }\n\n      .unknown-component {\n        background: var(--vanna-background-higher);\n        border: 1px solid var(--vanna-outline-default);\n        border-radius: var(--vanna-border-radius-md);\n        padding: var(--vanna-space-4);\n        font-family: var(--vanna-font-family-mono);\n        font-size: 12px;\n      }\n\n      .unknown-component p {\n        margin: 0 0 var(--vanna-space-2) 0;\n        color: var(--vanna-foreground-dimmer);\n      }\n\n      .unknown-component pre {\n        margin: 0;\n        color: var(--vanna-foreground-dimmest);\n        overflow-x: auto;\n      }\n\n      .chat-input-area {\n        padding: var(--vanna-space-5) var(--vanna-space-6) var(--vanna-space-6);\n        background: var(--chat-surface);\n        border-top: 1px solid var(--chat-outline);\n        display: flex;\n        flex-direction: column;\n        gap: var(--vanna-space-4);\n        flex-shrink: 0; /* Prevent input area from shrinking */\n      }\n\n      :host([theme=\"dark\"]) .chat-input-area {\n        border-top-color: rgba(148, 163, 184, 0.22);\n      }\n\n      .chat-input-container {\n        display: flex;\n        align-items: center;\n        gap: var(--vanna-space-2);\n        padding: 6px 8px 6px 18px;\n        border-radius: 999px;\n        background: var(--chat-muted);\n        border: 1px solid var(--chat-muted-stronger);\n        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6);\n        transition: border-color var(--vanna-duration-200) ease, box-shadow var(--vanna-duration-200) ease, background var(--vanna-duration-200) ease;\n      }\n\n      .chat-input-container:focus-within {\n        border-color: var(--chat-primary);\n        box-shadow: 0 0 0 1px rgba(99, 102, 241, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.85);\n        background: rgba(255, 255, 255, 0.95);\n      }\n\n      :host([theme=\"dark\"]) .chat-input-container {\n        background: rgba(15, 23, 42, 0.65);\n        border-color: rgba(100, 116, 139, 0.45);\n        box-shadow: inset 0 1px 0 rgba(148, 163, 184, 0.18);\n      }\n\n      :host([theme=\"dark\"]) .chat-input-container:focus-within {\n        border-color: rgba(129, 140, 248, 0.55);\n        box-shadow: 0 0 0 1px rgba(129, 140, 248, 0.45), inset 0 1px 0 rgba(148, 163, 184, 0.25);\n        background: rgba(30, 41, 59, 0.88);\n      }\n\n      .message-input {\n        flex: 1;\n        border: none;\n        background: transparent;\n        font-size: 15px;\n        font-family: var(--vanna-font-family-default);\n        line-height: 1.5;\n        color: var(--vanna-foreground-default);\n        resize: none;\n        min-height: 48px;\n        max-height: 140px;\n        padding: 12px 0;\n        outline: none;\n      }\n\n      :host([theme=\"dark\"]) .message-input {\n        color: rgba(226, 232, 240, 0.95);\n      }\n\n      .message-input::placeholder {\n        color: rgba(71, 85, 105, 0.8);\n      }\n\n      :host([theme=\"dark\"]) .message-input::placeholder {\n        color: rgba(148, 163, 184, 0.65);\n      }\n\n      .message-input:focus {\n        outline: none;\n      }\n\n      .message-input:disabled {\n        color: rgba(148, 163, 184, 0.65);\n        cursor: not-allowed;\n      }\n\n      :host([theme=\"dark\"]) .message-input:disabled {\n        color: rgba(100, 116, 139, 0.55);\n      }\n\n      .send-button {\n        width: 48px;\n        height: 48px;\n        border-radius: 999px;\n        border: none;\n        background: linear-gradient(135deg, var(--chat-primary-stronger), var(--chat-primary));\n        color: var(--chat-primary-foreground);\n        display: inline-flex;\n        align-items: center;\n        justify-content: center;\n        cursor: pointer;\n        transition: transform var(--vanna-duration-200) ease, box-shadow var(--vanna-duration-200) ease, filter var(--vanna-duration-200) ease;\n        box-shadow: 0 18px 38px -24px rgba(79, 70, 229, 0.8);\n      }\n\n      .send-button:hover {\n        transform: translateY(-1px) scale(1.02);\n        box-shadow: 0 25px 45px -24px rgba(79, 70, 229, 0.85);\n      }\n\n      .send-button:active {\n        transform: translateY(0) scale(0.98);\n      }\n\n      .send-button:disabled {\n        background: rgba(148, 163, 184, 0.35);\n        color: rgba(71, 85, 105, 0.7);\n        cursor: not-allowed;\n        transform: none;\n        box-shadow: none;\n      }\n\n      .send-button svg {\n        width: 18px;\n        height: 18px;\n      }\n\n      .sidebar {\n        background: linear-gradient(180deg, rgba(99, 102, 241, 0.08) 0%, rgba(15, 23, 42, 0.02) 100%);\n        padding: var(--vanna-space-6);\n        display: flex;\n        flex-direction: column;\n        gap: var(--vanna-space-4);\n        overflow-y: auto;\n        overflow-x: hidden;\n        min-height: 0;\n      }\n\n      .sidebar::-webkit-scrollbar {\n        width: 6px;\n      }\n\n      .sidebar::-webkit-scrollbar-track {\n        background: transparent;\n      }\n\n      .sidebar::-webkit-scrollbar-thumb {\n        background: var(--vanna-outline-default);\n        border-radius: var(--vanna-border-radius-full);\n      }\n\n      :host([theme=\"dark\"]) .sidebar {\n        background: linear-gradient(180deg, rgba(79, 70, 229, 0.22) 0%, rgba(15, 23, 42, 0.45) 100%);\n      }\n\n      .empty-state {\n        display: flex;\n        flex-direction: column;\n        align-items: center;\n        justify-content: center;\n        text-align: center;\n        color: var(--vanna-foreground-dimmer);\n        padding: var(--vanna-space-12) var(--vanna-space-8);\n        margin: var(--vanna-space-8) var(--vanna-space-6);\n        font-size: 15px;\n        font-weight: 500;\n        line-height: 1.6;\n        background: linear-gradient(135deg, \n          rgba(255, 255, 255, 0.95) 0%, \n          rgba(248, 250, 252, 0.9) 50%,\n          rgba(241, 245, 249, 0.85) 100%);\n        border-radius: var(--vanna-border-radius-2xl);\n        border: 2px dashed var(--vanna-accent-primary-default);\n        box-shadow: \n          var(--vanna-shadow-sm),\n          inset 0 1px 0 rgba(255, 255, 255, 0.8);\n        backdrop-filter: blur(8px);\n        transition: all var(--vanna-duration-300) ease;\n      }\n\n      .empty-state:hover {\n        border-color: var(--vanna-accent-primary-stronger);\n        transform: translateY(-2px);\n        box-shadow: \n          var(--vanna-shadow-lg),\n          inset 0 1px 0 rgba(255, 255, 255, 0.9);\n      }\n\n      :host([theme=\"dark\"]) .empty-state {\n        color: var(--vanna-foreground-dimmer);\n        background: linear-gradient(135deg, \n          rgba(24, 29, 39, 0.95) 0%, \n          rgba(31, 39, 51, 0.9) 50%,\n          rgba(17, 21, 28, 0.85) 100%);\n        border-color: var(--vanna-accent-primary-default);\n        box-shadow: \n          var(--vanna-shadow-md),\n          inset 0 1px 0 rgba(129, 140, 248, 0.2);\n      }\n\n      :host([theme=\"dark\"]) .empty-state:hover {\n        border-color: var(--vanna-accent-primary-hover);\n        box-shadow: \n          var(--vanna-shadow-xl),\n          inset 0 1px 0 rgba(129, 140, 248, 0.3);\n      }\n\n      .empty-state-icon {\n        width: 64px;\n        height: 64px;\n        margin: 0 auto var(--vanna-space-6);\n        opacity: 0.7;\n        color: var(--vanna-accent-primary-default);\n        filter: drop-shadow(0 2px 4px rgba(79, 70, 229, 0.2));\n      }\n\n      .empty-state-text {\n        font-size: 16px;\n        font-weight: 600;\n        color: var(--vanna-foreground-default);\n        margin-bottom: var(--vanna-space-2);\n      }\n\n      .empty-state-subtitle {\n        font-size: 14px;\n        color: var(--vanna-foreground-dimmest);\n        opacity: 0.8;\n        font-weight: 400;\n      }\n\n      @media (max-width: 880px) {\n        .chat-layout {\n          grid-template-columns: 1fr;\n          height: min(600px, 85vh);\n          max-height: 85vh;\n        }\n\n        .sidebar {\n          display: none;\n        }\n\n        .chat-main {\n          border-right: none;\n        }\n      }\n\n      @media (max-width: 600px) {\n        :host {\n          border-radius: var(--vanna-border-radius-xl);\n        }\n\n        .chat-layout {\n          height: min(500px, 80vh);\n          max-height: 80vh;\n        }\n\n        .chat-header {\n          border-bottom-width: 0;\n          padding: var(--vanna-space-5) var(--vanna-space-5) var(--vanna-space-4);\n        }\n\n        .chat-messages {\n          padding: var(--vanna-space-4) var(--vanna-space-4);\n        }\n\n        .empty-state {\n          padding: var(--vanna-space-10) var(--vanna-space-6);\n          margin: var(--vanna-space-6) var(--vanna-space-4);\n          font-size: 14px;\n        }\n\n        .empty-state-text {\n          font-size: 15px;\n        }\n\n        .empty-state-icon {\n          width: 56px;\n          height: 56px;\n          margin-bottom: var(--vanna-space-5);\n        }\n\n        .chat-input-area {\n          padding: var(--vanna-space-4) var(--vanna-space-4) var(--vanna-space-5);\n        }\n      }\n    `\n  ];\n\n  @property() title = 'Vanna AI Chat';\n  @property() placeholder = 'Ask me anything...';\n  @property({ type: Boolean }) disabled = false;\n  @property({ type: Boolean }) showProgress = true;\n  @property({ type: Boolean }) allowMinimize = true;\n  @property({ reflect: true }) theme = 'light';\n  @property({ attribute: 'api-base' }) apiBaseUrl = '';\n  @property({ attribute: 'sse-endpoint' }) sseEndpoint = '/api/vanna/v2/chat_sse';\n  @property({ attribute: 'ws-endpoint' }) wsEndpoint = '/api/vanna/v2/chat_websocket';\n  @property({ attribute: 'poll-endpoint' }) pollEndpoint = '/api/vanna/v2/chat_poll';\n  @property() subtitle = '';\n  @property() startingState: 'normal' | 'maximized' | 'minimized' = 'normal';\n\n  @state() private currentMessage = '';\n  @state() private status: 'idle' | 'working' | 'error' | 'success' = 'idle';\n  @state() private statusMessage = '';\n  @state() private statusDetail = '';\n  private _windowState: 'normal' | 'maximized' | 'minimized' = 'normal';\n\n  @property({ reflect: false })\n  get windowState() {\n    return this._windowState;\n  }\n\n  set windowState(value: 'normal' | 'maximized' | 'minimized') {\n    console.log('windowState setter called with:', value);\n    console.trace('Call stack:');\n    const oldValue = this._windowState;\n    this._windowState = value;\n    this.requestUpdate('windowState', oldValue);\n  }\n\n  private apiClient!: VannaApiClient;\n  private conversationId: string;\n  private componentManager: ComponentManager | null = null;\n  private componentObserver: MutationObserver | null = null;\n\n  constructor() {\n    super();\n    // Note: Don't create apiClient here - attributes haven't been set yet!\n    // It will be created lazily in getApiClient() or firstUpdated()\n    this.conversationId = this.generateId();\n  }\n\n  /**\n   * Ensure API client is created/updated with current endpoint values\n   */\n  private ensureApiClient() {\n    // Always recreate to ensure we have the latest endpoint values\n    console.log('[VannaChat] Creating API client with:', {\n      baseUrl: this.apiBaseUrl,\n      sseEndpoint: this.sseEndpoint,\n      wsEndpoint: this.wsEndpoint,\n      pollEndpoint: this.pollEndpoint\n    });\n\n    this.apiClient = new VannaApiClient({\n      baseUrl: this.apiBaseUrl,\n      sseEndpoint: this.sseEndpoint,\n      wsEndpoint: this.wsEndpoint,\n      pollEndpoint: this.pollEndpoint\n    });\n  }\n\n  firstUpdated() {\n    // Create API client now that attributes have been set\n    this.ensureApiClient();\n\n    // Initialize component manager with rich components container (fallback)\n    const richContainer = this.shadowRoot?.querySelector('.rich-components-container') as HTMLElement;\n    if (richContainer) {\n      this.componentManager = new ComponentManager(richContainer);\n      \n      // Watch for changes in the rich components container to manage empty state\n      this.componentObserver = new MutationObserver(() => {\n        // Update empty state visibility\n        this.updateEmptyState();\n      });\n      \n      this.componentObserver.observe(richContainer, {\n        childList: true,\n        subtree: true,\n        attributes: false\n      });\n    }\n\n    // Set initial window state from startingState property\n    if (this.startingState !== 'normal') {\n      this._windowState = this.startingState;\n    }\n\n    // Set initial CSS class\n    this.classList.add(this._windowState);\n\n    // Request starter UI from backend\n    this.requestStarterUI();\n  }\n\n  /**\n   * Request starter UI (buttons, welcome messages) from backend\n   */\n  private async requestStarterUI(): Promise<void> {\n    try {\n      const request = {\n        message: \"\",\n        conversation_id: this.conversationId,\n        request_id: this.generateId(),\n        metadata: {\n          starter_ui_request: true\n        }\n      };\n\n      // Stream the starter UI response\n      await this.handleStreamingResponse(request);\n    } catch (error) {\n      console.error('Error requesting starter UI:', error);\n      // Fail silently - starter UI is optional\n    }\n  }\n\n  disconnectedCallback() {\n    super.disconnectedCallback();\n    \n    // Clean up mutation observer\n    if (this.componentObserver) {\n      this.componentObserver.disconnect();\n      this.componentObserver = null;\n    }\n  }\n\n  updated(changedProperties: Map<string, any>) {\n    super.updated(changedProperties);\n\n    // Update host classes based on window state\n    if (changedProperties.has('windowState')) {\n      console.log('windowState changed to:', this._windowState);\n      this.classList.remove('normal', 'maximized', 'minimized');\n      this.classList.add(this._windowState);\n      console.log('Applied CSS classes:', this.className);\n    }\n  }\n\n  private handleInput(e: Event) {\n    const input = e.target as HTMLInputElement;\n    this.currentMessage = input.value;\n  }\n\n  private handleKeyPress(e: KeyboardEvent) {\n    if (e.key === 'Enter' && !e.shiftKey) {\n      e.preventDefault();\n      this.sendMessage();\n    }\n  }\n\n  /**\n   * Send a message programmatically (can be called from buttons or external code)\n   * Returns a Promise that resolves with success status\n   */\n  sendMessage(messageText?: string): Promise<boolean> {\n    console.log('sendMessage called with:', messageText);\n\n    // Use provided message or fall back to current input\n    // Check if messageText is actually a string (not an event object)\n    const textToSend = (typeof messageText === 'string') ? messageText : this.currentMessage;\n\n    console.log('Will send:', textToSend);\n\n    if (!textToSend.trim() || this.disabled) {\n      console.log('Message empty or disabled, not sending');\n      return Promise.resolve(false);\n    }\n\n    return this._sendMessageInternal(textToSend);\n  }\n\n  private async _sendMessageInternal(messageText: string): Promise<boolean> {\n    console.log('_sendMessageInternal called with:', messageText);\n\n    // Auto-maximize window when user sends a message (if not already maximized or minimized)\n    if (this.windowState !== 'maximized' && this.windowState !== 'minimized') {\n      this.maximizeWindow();\n    }\n\n    // Create user message as a rich component and send to ComponentManager\n    const userRichComponent: RichComponent = {\n      id: `user-message-${Date.now()}`,\n      type: 'user-message',\n      lifecycle: 'create',\n      data: {\n        content: messageText,\n        sender: 'user'\n      },\n      children: [],\n      timestamp: new Date().toISOString(),\n      visible: true,\n      interactive: false\n    };\n\n    // Add user message to ComponentManager for chronological ordering\n    if (this.componentManager) {\n      const update = {\n        operation: 'create' as const,\n        target_id: userRichComponent.id,\n        component: userRichComponent,\n        timestamp: userRichComponent.timestamp\n      };\n      this.componentManager.processUpdate(update);\n    }\n\n    // Update empty state after a brief delay to let ComponentManager render\n    setTimeout(() => this.updateEmptyState(), 0);\n\n    console.log('Added user message as rich component to ComponentManager:', userRichComponent);\n\n    // Update the view\n    this.requestUpdate();\n\n    // Update status to working (initial frontend status before backend responds)\n    this.setStatus('working', 'Sending message...', '');\n\n    // Clear input only if we're sending from the input field\n    if (messageText === this.currentMessage) {\n      this.currentMessage = '';\n      const input = this.shadowRoot?.querySelector('.message-input') as HTMLTextAreaElement;\n      if (input) {\n        input.value = '';\n        input.style.height = 'auto';\n      }\n    }\n\n    // Dispatch event for external listeners\n    this.dispatchEvent(new CustomEvent('message-sent', {\n      detail: { message: { content: messageText, type: 'user' } },\n      bubbles: true,\n      composed: true\n    }));\n\n    try {\n      // Create the request\n      const request = {\n        message: messageText,\n        conversation_id: this.conversationId,\n        request_id: this.generateId(),\n        metadata: {}\n      };\n\n      // Stream the response\n      await this.handleStreamingResponse(request);\n      return true; // Success\n\n    } catch (error) {\n      console.error('Error sending message:', error);\n      this.setStatus('error', 'Failed to send message', error instanceof Error ? error.message : 'Unknown error');\n\n      // Add error message\n      this.addMessage(\n        `Sorry, I encountered an error: ${error instanceof Error ? error.message : 'Unknown error'}`,\n        'assistant'\n      );\n      return false; // Failure\n    }\n  }\n\n  private getTitleInitials(): string {\n    const title = (this.title || '').trim();\n    if (!title) {\n      return 'VA';\n    }\n\n    const parts = title.split(/\\s+/).filter(Boolean);\n    if (parts.length === 1) {\n      return parts[0].charAt(0).toUpperCase() || 'V';\n    }\n\n    const first = parts[0].charAt(0);\n    const last = parts[parts.length - 1].charAt(0);\n    const initials = `${first}${last}`.toUpperCase();\n    return initials || 'VA';\n  }\n\n  private minimizeWindow(e?: Event) {\n    if (e) {\n      e.stopPropagation();\n      e.preventDefault();\n    }\n    console.log('minimizeWindow called, current state:', this._windowState);\n    this.windowState = 'minimized';\n    console.log('minimizeWindow set state to:', this._windowState);\n    this.dispatchEvent(new CustomEvent('window-state-changed', {\n      detail: { state: 'minimized' },\n      bubbles: true,\n      composed: true\n    }));\n  }\n\n  private maximizeWindow(e?: Event) {\n    if (e) {\n      e.stopPropagation();\n      e.preventDefault();\n    }\n    this.windowState = 'maximized';\n    this.dispatchEvent(new CustomEvent('window-state-changed', {\n      detail: { state: 'maximized' },\n      bubbles: true,\n      composed: true\n    }));\n  }\n\n  private restoreWindow(e?: Event) {\n    if (e) {\n      e.stopPropagation();\n      e.preventDefault();\n    }\n    this.windowState = 'normal';\n    this.dispatchEvent(new CustomEvent('window-state-changed', {\n      detail: { state: 'normal' },\n      bubbles: true,\n      composed: true\n    }));\n  }\n\n\n  addMessage(content: string, type: 'user' | 'assistant') {\n    // Create message as a rich component and send to ComponentManager\n    const richComponent: RichComponent = {\n      id: `${type}-message-${Date.now()}`,\n      type: `${type}-message`,\n      lifecycle: 'create',\n      data: {\n        content: content,\n        sender: type\n      },\n      children: [],\n      timestamp: new Date().toISOString(),\n      visible: true,\n      interactive: false\n    };\n\n    if (this.componentManager) {\n      const update = {\n        operation: 'create' as const,\n        target_id: richComponent.id,\n        component: richComponent,\n        timestamp: richComponent.timestamp\n      };\n      this.componentManager.processUpdate(update);\n    }\n  }\n\n  setStatus(status: typeof this.status, message: string, detail?: string) {\n    this.status = status;\n    this.statusMessage = message;\n    this.statusDetail = detail || '';\n  }\n\n  clearStatus() {\n    this.statusMessage = '';\n    this.statusDetail = '';\n    this.status = 'idle';\n  }\n\n  getProgressTracker(): HTMLElement | null {\n    return this.shadowRoot?.querySelector('vanna-progress-tracker') || null;\n  }\n\n  private async handleStreamingResponse(request: any) {\n    // Ensure API client exists and is up to date\n    if (!this.apiClient || this.apiClient.baseUrl !== this.apiBaseUrl) {\n      this.ensureApiClient();\n    }\n\n    // Note: Status bar updates are now controlled by backend via StatusBarUpdateComponent\n    // Frontend only shows initial \"Sending message...\" status (set in _sendMessageInternal)\n    // and handles connection errors below\n\n    try {\n      // Use SSE streaming by default\n      const stream = this.apiClient.streamChat(request);\n\n      for await (const chunk of stream) {\n        await this.processChunk(chunk);\n      }\n\n      // Backend is responsible for final status via StatusBarUpdateComponent\n      // No frontend status clearing here\n\n    } catch (error) {\n      console.warn('SSE streaming failed, falling back to polling:', error);\n\n      try {\n        // Fallback to polling - show user we're retrying\n        this.setStatus('working', 'Connection issue, retrying...', 'Using fallback method');\n        const response = await this.apiClient.sendPollMessage(request);\n\n        for (const chunk of response.chunks) {\n          await this.processChunk(chunk);\n        }\n\n        // Backend is responsible for final status via StatusBarUpdateComponent\n\n      } catch (pollError) {\n        // Only set error status if polling also fails (connection error)\n        this.setStatus('error', 'Connection failed', 'Unable to reach server');\n        throw pollError;\n      }\n    }\n  }\n\n  private async processChunk(chunk: ChatStreamChunk) {\n    // Dispatch chunk event for external listeners\n    this.dispatchEvent(new CustomEvent('chunk-received', {\n      detail: { chunk },\n      bubbles: true,\n      composed: true\n    }));\n\n    console.log('Processing chunk:', chunk); // Debug log\n\n    // Handle rich components via ComponentManager\n    if (chunk.rich && this.componentManager) {\n      console.log('Processing rich component via ComponentManager:', chunk.rich); // Debug log\n      \n      if (chunk.rich.id && chunk.rich.lifecycle) {\n        // Standard rich component with lifecycle\n        const component = chunk.rich as RichComponent;\n        const update = {\n          operation: chunk.rich.lifecycle as any,\n          target_id: chunk.rich.id,\n          component: component,\n          timestamp: new Date().toISOString()\n        };\n        this.componentManager.processUpdate(update);\n      } else if (chunk.rich.type === 'component_update') {\n        // Component update format\n        this.componentManager.processUpdate(chunk.rich as any);\n      } else {\n        // Generic rich component\n        const component = chunk.rich as RichComponent;\n        const update = {\n          operation: 'create' as const,\n          target_id: component.id || `component-${Date.now()}`,\n          component: component,\n          timestamp: new Date().toISOString()\n        };\n        this.componentManager.processUpdate(update);\n      }\n      \n      return;\n    }\n\n    // Update progress tracker for legacy components (keep for backward compatibility)\n    const progressTracker = this.getProgressTracker();\n    if (progressTracker && 'addStep' in progressTracker) {\n      (progressTracker as any).addStep({\n        id: `chunk-${Date.now()}`,\n        title: this.getChunkTitle(chunk),\n        status: 'completed',\n        timestamp: chunk.timestamp\n      });\n    }\n\n    // Handle different chunk types (legacy components)\n    const componentType = chunk.rich?.type;\n    switch (componentType) {\n      case 'text':\n        // Text chunks are handled in the main loop\n        break;\n\n      case 'thinking':\n        // Legacy: Status bar updates now handled by backend via StatusBarUpdateComponent\n        // This case is kept for backward compatibility but doesn't update status\n        break;\n\n      case 'tool_execution':\n        // Legacy: Status bar updates now handled by backend via StatusBarUpdateComponent\n        // This case is kept for backward compatibility but doesn't update status\n        break;\n\n      case 'error':\n        throw new Error(chunk.rich.data?.message || 'Unknown error from agent');\n\n      default:\n        // Handle other component types as needed\n        console.log('Received chunk:', componentType, chunk.rich);\n    }\n  }\n\n\n  private getChunkTitle(chunk: ChatStreamChunk): string {\n    const componentType = chunk.rich?.type;\n    switch (componentType) {\n      case 'text':\n        return 'Generating response';\n      case 'thinking':\n        return 'Thinking';\n      case 'tool_execution':\n        return `Tool: ${chunk.rich.data?.tool_name || 'Unknown'}`;\n      default:\n        return `Processing ${componentType || 'component'}`;\n    }\n  }\n\n  private generateId(): string {\n    return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;\n  }\n\n  /**\n   * Update the API base URL and recreate the client\n   */\n  updateApiBaseUrl(baseUrl: string) {\n    this.apiBaseUrl = baseUrl;\n    this.ensureApiClient();\n  }\n\n  /**\n   * Get the API client instance for direct access\n   */\n  getApiClient(): VannaApiClient {\n    if (!this.apiClient) {\n      this.ensureApiClient();\n    }\n    return this.apiClient;\n  }\n\n  /**\n   * Set custom headers for authentication or other purposes\n   */\n  setCustomHeaders(headers: Record<string, string>) {\n    this.apiClient.setCustomHeaders(headers);\n  }\n\n  /**\n   * Update empty state visibility based on whether there are components\n   */\n  private updateEmptyState() {\n    const emptyState = this.shadowRoot?.querySelector('#empty-state') as HTMLElement;\n    const richContainer = this.shadowRoot?.querySelector('.rich-components-container') as HTMLElement;\n    \n    if (emptyState && richContainer) {\n      // Show empty state if rich container has no children\n      const hasContent = richContainer.children.length > 0;\n      emptyState.style.display = hasContent ? 'none' : 'flex';\n    }\n  }\n\n  /**\n   * Update scroll indicator based on scroll position\n   */\n  private updateScrollIndicator() {\n    const messagesContainer = this.shadowRoot?.querySelector('.chat-messages');\n    if (!messagesContainer) return;\n    \n    // Check if there's content scrolled above\n    const hasScrolledContent = messagesContainer.scrollTop > 10;\n    \n    // Update scroll indicator class\n    messagesContainer.classList.toggle('has-scroll', hasScrolledContent);\n  }\n\n  /**\n   * Scroll to the top of the last message/component that was added\n   * This always scrolls regardless of current scroll position\n   */\n  scrollToLastMessage() {\n    const messagesContainer = this.shadowRoot?.querySelector('.chat-messages');\n    const richContainer = this.shadowRoot?.querySelector('.rich-components-container');\n    \n    if (!messagesContainer || !richContainer) return;\n\n    // Get the last child element (the most recently added component)\n    const lastComponent = richContainer.lastElementChild as HTMLElement;\n    if (!lastComponent) return;\n\n    // Scroll so the top of the last component is visible\n    lastComponent.scrollIntoView({ behavior: 'smooth', block: 'start' });\n    \n    // Update scroll indicator after scrolling\n    setTimeout(() => this.updateScrollIndicator(), 100);\n  }\n\n  /**\n   * Clear all messages (useful for testing)\n   */\n  clearMessages() {\n    if (this.componentManager) {\n      this.componentManager.clear();\n    }\n    this.updateEmptyState();\n    this.requestUpdate();\n  }\n\n  /**\n   * Add multiple messages at once (useful for testing scrolling)\n   */\n  addTestMessages(count: number = 10) {\n    for (let i = 1; i <= count; i++) {\n      setTimeout(() => {\n        const type = i % 2 === 0 ? 'assistant' : 'user';\n        const content = `This is test message number ${i}. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.`;\n        this.addMessage(content, type);\n      }, i * 100); // Stagger the messages to simulate real timing\n    }\n  }\n\n  render() {\n    return html`\n      <!-- Minimized icon - shown only when minimized via CSS and allowMinimize is true -->\n      ${this.allowMinimize ? html`\n        <div class=\"minimized-icon\" @click=${this.restoreWindow}>\n          <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"32\" height=\"32\">\n            <path d=\"M20 2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z\"/>\n          </svg>\n        </div>\n      ` : ''}\n\n      <!-- Main chat interface -->\n      <div class=\"chat-layout ${this.showProgress ? '' : 'compact'}\">\n        <div class=\"chat-main\">\n          <div class=\"chat-header\">\n            <div class=\"header-top\">\n              <div class=\"header-left\">\n                <div class=\"chat-avatar\" aria-hidden=\"true\">${this.getTitleInitials()}</div>\n                <div class=\"header-text\">\n                  <h2 class=\"chat-title\">${this.title}</h2>\n                </div>\n              </div>\n              <div class=\"header-top-actions\">\n                <div class=\"window-controls\">\n                  ${this.allowMinimize ? html`\n                    <button\n                      class=\"window-control-btn minimize\"\n                      @click=${this.minimizeWindow}\n                      title=\"Minimize\">\n                      <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n                        <path d=\"M5 12h14v2H5z\"/>\n                      </svg>\n                    </button>\n                  ` : ''}\n                  ${this.windowState === 'maximized' ? html`\n                    <button\n                      class=\"window-control-btn restore\"\n                      @click=${this.restoreWindow}\n                      title=\"Restore\">\n                      <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n                        <path d=\"M8 8v2h2V8h6v6h-2v2h4V6H8zm-2 4v8h8v-2H8v-6H6z\"/>\n                      </svg>\n                    </button>\n                  ` : html`\n                    <button\n                      class=\"window-control-btn maximize\"\n                      @click=${this.maximizeWindow}\n                      title=\"Maximize\">\n                      <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n                        <path d=\"M5 5v14h14V5H5zm2 2h10v10H7V7z\"/>\n                      </svg>\n                    </button>\n                  `}\n                </div>\n              </div>\n            </div>\n          </div>\n\n          <div class=\"chat-messages\">\n            <!-- Empty state - shown when no components exist -->\n            <div class=\"empty-state\" id=\"empty-state\">\n              <div class=\"empty-state-icon\">\n                <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n                  <path d=\"M20 2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z\"/>\n                </svg>\n              </div>\n              <div class=\"empty-state-text\">Start a conversation</div>\n              <div class=\"empty-state-subtitle\">Type your message below to begin chatting</div>\n            </div>\n\n            <!-- Rich Components Container - all content renders here via ComponentManager -->\n            <div class=\"rich-components-container\"></div>\n          </div>\n\n          <div class=\"chat-input-area\">\n            <vanna-status-bar\n              .status=${this.status}\n              .message=${this.statusMessage}\n              .detail=${this.statusDetail}\n              theme=${this.theme}>\n            </vanna-status-bar>\n\n            <div class=\"chat-input-container\">\n              <textarea\n                class=\"message-input\"\n                .placeholder=${this.placeholder}\n                .disabled=${this.disabled}\n                @input=${this.handleInput}\n                @keydown=${this.handleKeyPress}\n                rows=\"1\"\n              ></textarea>\n              <button\n                class=\"send-button\"\n                type=\"button\"\n                aria-label=\"Send message\"\n                .disabled=${this.disabled || !this.currentMessage.trim()}\n                @click=${this.sendMessage}\n              >\n                <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n                  <path d=\"M2.01 21L23 12 2.01 3 2 10l15 2-15 2z\"/>\n                </svg>\n              </button>\n            </div>\n          </div>\n        </div>\n\n        ${this.showProgress ? html`\n          <div class=\"sidebar\">\n            <vanna-progress-tracker theme=${this.theme}></vanna-progress-tracker>\n          </div>\n        ` : ''}\n      </div>\n    `;\n  }\n}\n"
  },
  {
    "path": "frontends/webcomponent/src/components/vanna-message.stories.ts",
    "content": "import type { Meta, StoryObj } from '@storybook/web-components';\nimport { html } from 'lit';\nimport './vanna-message';\n\nconst meta: Meta = {\n  title: 'Components/VannaMessage',\n  component: 'vanna-message',\n  parameters: {\n    layout: 'centered',\n  },\n  argTypes: {\n    content: { control: 'text' },\n    type: {\n      control: 'select',\n      options: ['user', 'assistant'],\n    },\n    timestamp: { control: 'number' },\n  },\n};\n\nexport default meta;\ntype Story = StoryObj;\n\nexport const UserMessage: Story = {\n  args: {\n    content: 'Hello! Can you help me analyze my data?',\n    type: 'user',\n    timestamp: Date.now(),\n  },\n  render: (args) => html`\n    <div style=\"width: 400px;\">\n      <vanna-message\n        .content=${args.content}\n        .type=${args.type}\n        .timestamp=${args.timestamp}>\n      </vanna-message>\n    </div>\n  `,\n};\n\nexport const AssistantMessage: Story = {\n  args: {\n    content: 'Of course! I\\'d be happy to help you analyze your data. Could you please tell me more about the type of data you have and what insights you\\'re looking for?',\n    type: 'assistant',\n    timestamp: Date.now(),\n  },\n  render: (args) => html`\n    <div style=\"width: 400px;\">\n      <vanna-message\n        .content=${args.content}\n        .type=${args.type}\n        .timestamp=${args.timestamp}>\n      </vanna-message>\n    </div>\n  `,\n};\n\nexport const LongMessage: Story = {\n  args: {\n    content: 'This is a very long message that demonstrates how the component handles longer text content. It should wrap properly and maintain good readability while staying within the maximum width constraints. The message can contain multiple sentences and paragraphs of information that the AI assistant might provide in response to complex queries.',\n    type: 'assistant',\n    timestamp: Date.now(),\n  },\n  render: (args) => html`\n    <div style=\"width: 400px;\">\n      <vanna-message\n        .content=${args.content}\n        .type=${args.type}\n        .timestamp=${args.timestamp}>\n      </vanna-message>\n    </div>\n  `,\n};\n\nexport const Conversation: Story = {\n  render: () => html`\n    <div style=\"width: 400px;\">\n      <vanna-message\n        content=\"What's the total revenue for Q4?\"\n        type=\"user\"\n        .timestamp=${Date.now() - 120000}>\n      </vanna-message>\n      <vanna-message\n        content=\"I'll help you calculate the total revenue for Q4. Let me query your database for this information.\"\n        type=\"assistant\"\n        .timestamp=${Date.now() - 60000}>\n      </vanna-message>\n      <vanna-message\n        content=\"The total revenue for Q4 is $2,450,000. This represents a 15% increase compared to Q3.\"\n        type=\"assistant\"\n        .timestamp=${Date.now()}>\n      </vanna-message>\n    </div>\n  `,\n};"
  },
  {
    "path": "frontends/webcomponent/src/components/vanna-message.ts",
    "content": "import { LitElement, html, css } from 'lit';\nimport { customElement, property } from 'lit/decorators.js';\nimport { vannaDesignTokens } from '../styles/vanna-design-tokens.js';\n\n@customElement('vanna-message')\nexport class VannaMessage extends LitElement {\n  static styles = [\n    vannaDesignTokens,\n    css`\n      :host {\n        display: block;\n        padding: 0 var(--vanna-space-2);\n        margin-bottom: var(--vanna-space-4);\n        font-family: var(--vanna-font-family-default);\n        animation: fade-in-up 0.25s ease-out;\n      }\n\n      :host(:last-of-type) {\n        margin-bottom: 0;\n      }\n\n      @keyframes fade-in-up {\n        from {\n          opacity: 0;\n          transform: translateY(16px);\n        }\n        to {\n          opacity: 1;\n          transform: translateY(0);\n        }\n      }\n\n      .message {\n        position: relative;\n        padding: var(--vanna-space-4) var(--vanna-space-5);\n        border-radius: var(--vanna-chat-bubble-radius);\n        word-wrap: break-word;\n        line-height: 1.6;\n        display: flex;\n        flex-direction: column;\n        gap: var(--vanna-space-2);\n        max-width: min(85%, 580px);\n        transition: transform var(--vanna-duration-200) ease, box-shadow var(--vanna-duration-200) ease;\n        backdrop-filter: blur(8px);\n      }\n\n      .message.assistant {\n        background: var(--vanna-background-root);\n        border: 1px solid var(--vanna-outline-dimmer);\n        color: var(--vanna-foreground-default);\n        box-shadow: var(--vanna-shadow-sm);\n        border-radius: var(--vanna-chat-bubble-radius) var(--vanna-chat-bubble-radius) var(--vanna-chat-bubble-radius) var(--vanna-space-2);\n      }\n\n      .message.user {\n        margin-left: auto;\n        max-width: min(80%, 500px);\n        background: linear-gradient(135deg, var(--vanna-accent-primary-stronger) 0%, var(--vanna-accent-primary-default) 100%);\n        color: white;\n        box-shadow: var(--vanna-shadow-md);\n        border-radius: var(--vanna-chat-bubble-radius) var(--vanna-chat-bubble-radius) var(--vanna-space-2) var(--vanna-chat-bubble-radius);\n        border: 1px solid rgba(255, 255, 255, 0.2);\n      }\n\n      .message:hover {\n        transform: translateY(-1px);\n      }\n\n      .message.assistant:hover {\n        box-shadow: var(--vanna-shadow-md);\n        border-color: var(--vanna-outline-hover);\n      }\n\n      .message.user:hover {\n        box-shadow: var(--vanna-shadow-lg);\n      }\n\n      .message-content {\n        margin: 0;\n        font-size: 15px;\n        letter-spacing: 0.01em;\n        white-space: pre-wrap;\n        font-weight: 400;\n      }\n\n      .message-content a {\n        color: inherit;\n        font-weight: 500;\n        text-decoration: underline;\n        text-decoration-thickness: 1px;\n        text-underline-offset: 2px;\n        opacity: 0.9;\n      }\n\n      .message-content code {\n        font-family: var(--vanna-font-family-mono);\n        background: var(--vanna-background-higher);\n        padding: 2px 6px;\n        border-radius: var(--vanna-border-radius-sm);\n        font-size: 13px;\n        border: 1px solid var(--vanna-outline-dimmer);\n      }\n\n      .message.user .message-content code {\n        background: rgba(255, 255, 255, 0.2);\n        border-color: rgba(255, 255, 255, 0.3);\n      }\n\n      .message-timestamp {\n        display: inline-flex;\n        align-items: center;\n        gap: var(--vanna-space-1);\n        font-size: 11px;\n        letter-spacing: 0.05em;\n        margin-top: var(--vanna-space-2);\n        font-family: var(--vanna-font-family-default);\n        opacity: 0.7;\n        font-weight: 500;\n      }\n\n      .message-timestamp::before {\n        content: '';\n        width: 3px;\n        height: 3px;\n        border-radius: var(--vanna-border-radius-full);\n        background: currentColor;\n        opacity: 0.8;\n      }\n\n      .message.assistant .message-timestamp {\n        align-self: flex-start;\n        color: var(--vanna-foreground-dimmest);\n      }\n\n      .message.assistant .message-timestamp::before {\n        background: var(--vanna-accent-primary-default);\n      }\n\n      .message.user .message-timestamp {\n        align-self: flex-end;\n        color: rgba(255, 255, 255, 0.8);\n      }\n\n      .message.user .message-timestamp::before {\n        background: rgba(255, 255, 255, 0.8);\n      }\n\n      :host([theme=\"dark\"]) .message.assistant {\n        background: var(--vanna-background-higher);\n        border: 1px solid var(--vanna-outline-default);\n        color: var(--vanna-foreground-default);\n        box-shadow: var(--vanna-shadow-md);\n      }\n\n      :host([theme=\"dark\"]) .message.assistant .message-content code {\n        background: var(--vanna-background-highest);\n        border-color: var(--vanna-outline-default);\n      }\n\n      :host([theme=\"dark\"]) .message.assistant .message-timestamp {\n        color: var(--vanna-foreground-dimmest);\n      }\n\n      :host([theme=\"dark\"]) .message.assistant .message-timestamp::before {\n        background: var(--vanna-accent-primary-default);\n      }\n\n      :host([theme=\"dark\"]) .message.user {\n        background: linear-gradient(135deg, var(--vanna-accent-primary-stronger) 0%, var(--vanna-accent-primary-default) 100%);\n        color: white;\n        box-shadow: var(--vanna-shadow-lg);\n      }\n\n      :host([theme=\"dark\"]) .message.user .message-content code {\n        background: rgba(255, 255, 255, 0.15);\n        border-color: rgba(255, 255, 255, 0.25);\n      }\n\n      :host([theme=\"dark\"]) .message.user .message-timestamp {\n        color: rgba(255, 255, 255, 0.8);\n      }\n\n      :host([theme=\"dark\"]) .message.user .message-timestamp::before {\n        background: rgba(255, 255, 255, 0.8);\n      }\n\n      @media (max-width: 600px) {\n        .message {\n          max-width: 100%;\n        }\n\n        .message.user {\n          max-width: 100%;\n        }\n      }\n    `\n  ];\n\n  @property() content = '';\n  @property() type: 'user' | 'assistant' = 'user';\n  @property({ type: Number }) timestamp = Date.now();\n  @property({ reflect: true }) theme = 'light';\n\n  private formatTimestamp(timestamp: number): string {\n    return new Date(timestamp).toLocaleTimeString([], {\n      hour: '2-digit',\n      minute: '2-digit'\n    });\n  }\n\n  render() {\n    return html`\n      <div class=\"message ${this.type}\">\n        <div class=\"message-content\">${this.content}</div>\n        <div class=\"message-timestamp\">\n          ${this.formatTimestamp(this.timestamp)}\n        </div>\n      </div>\n    `;\n  }\n}\n"
  },
  {
    "path": "frontends/webcomponent/src/components/vanna-progress-tracker.stories.ts",
    "content": "import type { Meta, StoryObj } from '@storybook/web-components';\nimport { html } from 'lit';\nimport './vanna-progress-tracker';\n\nconst meta: Meta = {\n  title: 'Components/VannaProgressTracker',\n  component: 'vanna-progress-tracker',\n  parameters: {\n    layout: 'centered',\n    backgrounds: {\n      default: 'light',\n      values: [\n        { name: 'dark', value: 'rgb(11, 15, 25)' },\n        { name: 'light', value: '#ffffff' },\n      ],\n    },\n  },\n  argTypes: {\n    title: { control: 'text' },\n    theme: {\n      control: 'select',\n      options: ['dark', 'light'],\n      description: 'Theme variant'\n    },\n  },\n};\n\nexport default meta;\ntype Story = StoryObj;\n\nexport const Empty: Story = {\n  args: {\n    title: 'Agent Progress',\n  },\n  render: (args) => html`\n    <div style=\"width: 350px; height: 400px;\">\n      <vanna-progress-tracker .title=${args.title}></vanna-progress-tracker>\n    </div>\n  `,\n};\n\nexport const WithTasks: Story = {\n  args: {\n    title: 'Agent Progress',\n    theme: 'light',\n  },\n  render: (args) => {\n    setTimeout(() => {\n      const tracker = document.querySelector('vanna-progress-tracker') as any;\n      if (tracker) {\n        tracker.addItem('Analyze database schema', 'Examining table structure');\n        tracker.addItem('Generate SQL query', 'Based on user request');\n        tracker.addItem('Execute query', 'Running against production DB');\n        tracker.addItem('Format results', 'Creating visualization');\n\n        // Update first item to in_progress\n        const items = tracker.shadowRoot?.querySelectorAll('.progress-item');\n        if (items?.[0]) {\n          tracker.updateItem(tracker.items[0].id, 'in_progress', 'Scanning tables...');\n        }\n      }\n    }, 100);\n\n    return html`\n      <div style=\"width: 350px; height: 400px; background: ${args.theme === 'light' ? '#ffffff' : 'rgb(11, 15, 25)'}; padding: 20px;\">\n        <vanna-progress-tracker .title=${args.title} theme=${args.theme}></vanna-progress-tracker>\n      </div>\n    `;\n  },\n};\n\nexport const WithTasksLight: Story = {\n  args: {\n    title: 'Agent Progress',\n    theme: 'light',\n  },\n  render: (args) => {\n    setTimeout(() => {\n      const tracker = document.querySelector('vanna-progress-tracker') as any;\n      if (tracker) {\n        tracker.addItem('Analyze database schema', 'Examining table structure');\n        tracker.addItem('Generate SQL query', 'Based on user request');\n        tracker.addItem('Execute query', 'Running against production DB');\n        tracker.addItem('Format results', 'Creating visualization');\n\n        // Update first item to in_progress\n        const items = tracker.shadowRoot?.querySelectorAll('.progress-item');\n        if (items?.[0]) {\n          tracker.updateItem(tracker.items[0].id, 'in_progress', 'Scanning tables...');\n        }\n      }\n    }, 100);\n\n    return html`\n      <div style=\"width: 350px; height: 400px; background: ${args.theme === 'light' ? '#ffffff' : 'rgb(11, 15, 25)'}; padding: 20px;\">\n        <vanna-progress-tracker .title=${args.title} theme=${args.theme}></vanna-progress-tracker>\n      </div>\n    `;\n  },\n};\n\nexport const MixedStatuses: Story = {\n  args: {\n    title: 'Data Analysis Pipeline',\n  },\n  render: (args) => {\n    setTimeout(() => {\n      const tracker = document.querySelector('vanna-progress-tracker') as any;\n      if (tracker) {\n        const id1 = tracker.addItem('Connect to database', 'Establishing connection');\n        const id2 = tracker.addItem('Validate credentials', 'Checking access permissions');\n        const id3 = tracker.addItem('Load data schema', 'Reading table definitions');\n        const id4 = tracker.addItem('Parse user query', 'Understanding natural language');\n        const id5 = tracker.addItem('Generate SQL', 'Converting to database query');\n        const id6 = tracker.addItem('Execute query', 'Running against database');\n        const id7 = tracker.addItem('Process results', 'Formatting output');\n\n        // Simulate different states\n        tracker.updateItem(id1, 'completed');\n        tracker.updateItem(id2, 'completed');\n        tracker.updateItem(id3, 'completed');\n        tracker.updateItem(id4, 'in_progress', 'Analyzing: \"Show me sales by region\"');\n        tracker.updateItem(id5, 'pending');\n        tracker.updateItem(id6, 'pending');\n        tracker.updateItem(id7, 'pending');\n      }\n    }, 100);\n\n    return html`\n      <div style=\"width: 350px; height: 400px;\">\n        <vanna-progress-tracker .title=${args.title}></vanna-progress-tracker>\n      </div>\n    `;\n  },\n};\n\nexport const WithError: Story = {\n  args: {\n    title: 'Query Processing',\n  },\n  render: (args) => {\n    setTimeout(() => {\n      const tracker = document.querySelector('vanna-progress-tracker') as any;\n      if (tracker) {\n        const id1 = tracker.addItem('Parse request', 'Understanding user query');\n        const id2 = tracker.addItem('Generate SQL', 'Creating database query');\n        const id3 = tracker.addItem('Execute query', 'Running against database');\n        tracker.addItem('Format results', 'Preparing visualization');\n\n        tracker.updateItem(id1, 'completed');\n        tracker.updateItem(id2, 'completed');\n        tracker.updateItem(id3, 'error', 'Table \"sales_data\" does not exist');\n        // id4 should remain pending due to error\n      }\n    }, 100);\n\n    return html`\n      <div style=\"width: 350px; height: 400px;\">\n        <vanna-progress-tracker .title=${args.title}></vanna-progress-tracker>\n      </div>\n    `;\n  },\n};\n\nexport const MultipleErrors: Story = {\n  args: {\n    title: 'Error Scenarios',\n  },\n  render: (args) => {\n    setTimeout(() => {\n      const tracker = document.querySelector('vanna-progress-tracker') as any;\n      if (tracker) {\n        const id1 = tracker.addItem('Connect to database', 'Establishing connection');\n        const id2 = tracker.addItem('Validate schema', 'Checking table structure');\n        const id3 = tracker.addItem('Parse SQL query', 'Analyzing syntax');\n        tracker.addItem('Execute query', 'Running database command');\n        tracker.addItem('Process results', 'Formatting output');\n\n        tracker.updateItem(id1, 'error', 'Connection timeout - database unreachable');\n        tracker.updateItem(id2, 'error', 'Invalid credentials provided');\n        tracker.updateItem(id3, 'error', 'Syntax error in SQL query');\n        // Other items remain pending\n      }\n    }, 100);\n\n    return html`\n      <div style=\"width: 350px; height: 400px;\">\n        <vanna-progress-tracker .title=${args.title}></vanna-progress-tracker>\n        <p style=\"font-size: 12px; color: #666; margin-top: 10px;\">\n          Example showing multiple error states with detailed error messages\n        </p>\n      </div>\n    `;\n  },\n};\n\nexport const LiveDemo: Story = {\n  args: {\n    title: 'Live Progress Demo',\n  },\n  render: (args) => {\n    let tracker: any;\n    let taskIds: string[] = [];\n    let currentIndex = 0;\n\n    const tasks = [\n      { text: 'Initialize AI agent', detail: 'Loading language model' },\n      { text: 'Analyze user request', detail: 'Processing natural language' },\n      { text: 'Query database schema', detail: 'Understanding data structure' },\n      { text: 'Generate SQL query', detail: 'Converting request to SQL' },\n      { text: 'Execute query', detail: 'Running against database' },\n      { text: 'Process results', detail: 'Formatting data for display' },\n      { text: 'Generate visualization', detail: 'Creating charts and graphs' }\n    ];\n\n    const runDemo = () => {\n      if (!tracker) {\n        tracker = document.querySelector('vanna-progress-tracker');\n        if (!tracker) {\n          setTimeout(runDemo, 100);\n          return;\n        }\n      }\n\n      // Add all tasks as pending\n      if (taskIds.length === 0) {\n        taskIds = tasks.map(task => tracker.addItem(task.text, task.detail));\n        currentIndex = 0;\n      }\n\n      // Process tasks one by one\n      if (currentIndex < tasks.length) {\n        // Mark current as in_progress\n        tracker.updateItem(taskIds[currentIndex], 'in_progress', `${tasks[currentIndex].detail}...`);\n\n        // Complete after 2 seconds, then move to next\n        setTimeout(() => {\n          tracker.updateItem(taskIds[currentIndex], 'completed');\n          currentIndex++;\n\n          // Continue with next task\n          if (currentIndex < tasks.length) {\n            setTimeout(runDemo, 500);\n          } else {\n            // Demo complete - restart after 3 seconds\n            setTimeout(() => {\n              tracker.clearItems();\n              taskIds = [];\n              currentIndex = 0;\n              setTimeout(runDemo, 1000);\n            }, 3000);\n          }\n        }, 2000);\n      }\n    };\n\n    setTimeout(runDemo, 500);\n\n    return html`\n      <div style=\"width: 350px; height: 400px;\">\n        <vanna-progress-tracker .title=${args.title}></vanna-progress-tracker>\n        <div style=\"margin-top: 10px; color: #999; font-size: 12px; text-align: center;\">\n          Watch tasks complete automatically (demo loops)\n        </div>\n      </div>\n    `;\n  },\n};"
  },
  {
    "path": "frontends/webcomponent/src/components/vanna-progress-tracker.ts",
    "content": "import { LitElement, html, css } from 'lit';\nimport { customElement, property, state } from 'lit/decorators.js';\nimport { vannaDesignTokens } from '../styles/vanna-design-tokens.js';\n\ninterface ProgressItem {\n  id: string;\n  text: string;\n  status: 'pending' | 'in_progress' | 'completed' | 'error';\n  detail?: string;\n}\n\n@customElement('vanna-progress-tracker')\nexport class VannaProgressTracker extends LitElement {\n  static styles = [\n    vannaDesignTokens,\n    css`\n      :host {\n        display: block;\n        background: var(--vanna-background-default);\n        border: 1px solid var(--vanna-outline-default);\n        border-radius: 0 0 var(--vanna-border-radius-lg) var(--vanna-border-radius-lg);\n        overflow: hidden;\n        font-family: var(--vanna-font-family-default);\n      }\n\n      .progress-label {\n        padding: var(--vanna-space-3) var(--vanna-space-4) var(--vanna-space-2);\n        display: flex;\n        align-items: center;\n        justify-content: space-between;\n      }\n\n      .progress-label-text {\n        font-size: 11px;\n        font-weight: 500;\n        color: var(--vanna-foreground-dimmest);\n        text-transform: uppercase;\n        letter-spacing: 0.5px;\n        margin: 0;\n      }\n\n      .progress-summary {\n        font-size: 10px;\n        color: var(--vanna-foreground-dimmest);\n        font-weight: 400;\n      }\n\n      .progress-list {\n        max-height: 300px;\n        overflow-y: auto;\n        padding-top: 0;\n      }\n\n      .progress-item {\n        padding: var(--vanna-space-3) var(--vanna-space-4);\n        border-bottom: 1px solid var(--vanna-outline-dimmest);\n        display: flex;\n        align-items: flex-start;\n        gap: var(--vanna-space-3);\n        transition: background var(--vanna-duration-150) ease;\n      }\n\n      .progress-item:last-child {\n        border-bottom: none;\n      }\n\n      .progress-item:hover {\n        background: var(--vanna-background-higher);\n      }\n\n      .progress-item.in_progress {\n        background: rgba(0, 123, 255, 0.05);\n        border-left: 3px solid var(--vanna-accent-primary-default);\n      }\n\n      .progress-item.completed {\n        opacity: 0.7;\n      }\n\n      .progress-item.error {\n        background: var(--vanna-accent-negative-subtle);\n        border-left: 3px solid var(--vanna-accent-negative-default);\n        padding-left: calc(var(--vanna-space-3) - 3px);\n      }\n\n      .progress-item.error .progress-text {\n        color: var(--vanna-accent-negative-stronger);\n      }\n\n      .progress-item.error .progress-detail {\n        color: var(--vanna-accent-negative-default);\n        font-weight: 500;\n      }\n\n      .progress-icon {\n        width: 16px;\n        height: 16px;\n        border-radius: 50%;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        flex-shrink: 0;\n        margin-top: 1px;\n      }\n\n      .progress-icon.pending {\n        background: var(--vanna-outline-default);\n      }\n\n      .progress-icon.in_progress {\n        background: var(--vanna-accent-primary-default);\n      }\n\n      .progress-icon.completed {\n        background: var(--vanna-accent-positive-default);\n      }\n\n      .progress-icon.error {\n        background: var(--vanna-accent-negative-default);\n        box-shadow: 0 0 0 2px var(--vanna-accent-negative-subtle);\n      }\n\n      .progress-icon svg {\n        width: 10px;\n        height: 10px;\n        color: white;\n      }\n\n      .progress-icon.error svg {\n        width: 8px;\n        height: 8px;\n        color: white;\n      }\n\n      .spinner-mini {\n        width: 10px;\n        height: 10px;\n        border: 1.5px solid rgba(255, 255, 255, 0.3);\n        border-top-color: white;\n        border-radius: 50%;\n        animation: spin 1s linear infinite;\n      }\n\n      .progress-content {\n        flex: 1;\n        min-width: 0;\n      }\n\n      .progress-text {\n        font-size: 13px;\n        color: var(--vanna-foreground-default);\n        font-weight: 500;\n        margin: 0 0 var(--vanna-space-1) 0;\n        line-height: 1.3;\n      }\n\n      .progress-detail {\n        font-size: 11px;\n        color: var(--vanna-foreground-dimmest);\n        margin: 0;\n        line-height: 1.3;\n      }\n\n      .empty-state {\n        padding: var(--vanna-space-6) var(--vanna-space-4);\n        text-align: center;\n        color: var(--vanna-foreground-dimmest);\n        font-size: 12px;\n      }\n\n      @keyframes spin {\n        to {\n          transform: rotate(360deg);\n        }\n      }\n    `\n  ];\n\n  @property() title = 'Progress';\n  @property() theme = 'light';\n  @state() private items: ProgressItem[] = [];\n\n  addItem(text: string, detail?: string, id?: string): string {\n    const itemId = id || Date.now().toString();\n    this.items = [...this.items, {\n      id: itemId,\n      text,\n      status: 'pending',\n      detail\n    }];\n    return itemId;\n  }\n\n  updateItem(id: string, status: ProgressItem['status'], detail?: string) {\n    this.items = this.items.map(item =>\n      item.id === id ? { ...item, status, detail } : item\n    );\n  }\n\n  clearItems() {\n    this.items = [];\n  }\n\n  private getStatusIcon(status: ProgressItem['status']) {\n    switch (status) {\n      case 'pending':\n        return html``;\n      case 'in_progress':\n        return html`<div class=\"spinner-mini\"></div>`;\n      case 'completed':\n        return html`\n          <svg viewBox=\"0 0 20 20\" fill=\"currentColor\">\n            <path fill-rule=\"evenodd\" d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\" clip-rule=\"evenodd\" />\n          </svg>\n        `;\n      case 'error':\n        return html`\n          <svg viewBox=\"0 0 20 20\" fill=\"currentColor\">\n            <path fill-rule=\"evenodd\" d=\"M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z\" clip-rule=\"evenodd\" />\n          </svg>\n        `;\n    }\n  }\n\n  private getProgressSummary() {\n    const completed = this.items.filter(item => item.status === 'completed').length;\n    const total = this.items.length;\n    const inProgress = this.items.filter(item => item.status === 'in_progress').length;\n\n    if (inProgress > 0) {\n      return `${completed}/${total} completed`;\n    }\n    return total > 0 ? `${completed}/${total} completed` : '';\n  }\n\n  render() {\n    return html`\n      ${this.items.length > 0 ? html`\n        <div class=\"progress-label\">\n          <span class=\"progress-label-text\">Tasks</span>\n          <span class=\"progress-summary\">${this.getProgressSummary()}</span>\n        </div>\n      ` : ''}\n\n      <div class=\"progress-list\">\n        ${this.items.length === 0\n          ? html`<div class=\"empty-state\">No tasks yet</div>`\n          : this.items.map(item => html`\n              <div class=\"progress-item ${item.status}\">\n                <div class=\"progress-icon ${item.status}\">\n                  ${this.getStatusIcon(item.status)}\n                </div>\n                <div class=\"progress-content\">\n                  <p class=\"progress-text\">${item.text}</p>\n                  ${item.detail ? html`<p class=\"progress-detail\">${item.detail}</p>` : ''}\n                </div>\n              </div>\n            `)\n        }\n      </div>\n    `;\n  }\n}"
  },
  {
    "path": "frontends/webcomponent/src/components/vanna-status-bar.stories.ts",
    "content": "import type { Meta, StoryObj } from '@storybook/web-components';\nimport { html } from 'lit';\nimport './vanna-status-bar';\n\nconst meta: Meta = {\n  title: 'Components/VannaStatusBar',\n  component: 'vanna-status-bar',\n  parameters: {\n    layout: 'centered',\n    backgrounds: {\n      default: 'light',\n      values: [\n        { name: 'dark', value: 'rgb(11, 15, 25)' },\n        { name: 'light', value: '#ffffff' },\n      ],\n    },\n  },\n  argTypes: {\n    status: {\n      control: 'select',\n      options: ['idle', 'working', 'error', 'success'],\n    },\n    message: { control: 'text' },\n    detail: { control: 'text' },\n    theme: {\n      control: 'select',\n      options: ['dark', 'light'],\n      description: 'Theme variant'\n    },\n  },\n};\n\nexport default meta;\ntype Story = StoryObj;\n\nexport const Idle: Story = {\n  args: {\n    status: 'idle',\n    message: '',\n    detail: '',\n  },\n  render: (args) => html`\n    <div style=\"width: 500px; padding: 20px;\">\n      <vanna-status-bar\n        .status=${args.status}\n        .message=${args.message}\n        .detail=${args.detail}>\n      </vanna-status-bar>\n      <div style=\"margin-top: 10px; color: #999; font-size: 12px;\">\n        Status bar is hidden when idle\n      </div>\n    </div>\n  `,\n};\n\nexport const Working: Story = {\n  args: {\n    status: 'working',\n    message: 'Analyzing your database schema...',\n    detail: 'Step 1 of 3',\n    theme: 'light',\n  },\n  render: (args) => html`\n    <div style=\"width: 500px; padding: 20px; background: ${args.theme === 'light' ? '#ffffff' : 'rgb(11, 15, 25)'};\">\n      <vanna-status-bar\n        .status=${args.status}\n        .message=${args.message}\n        .detail=${args.detail}\n        theme=${args.theme}>\n      </vanna-status-bar>\n    </div>\n  `,\n};\n\nexport const WorkingLight: Story = {\n  args: {\n    status: 'working',\n    message: 'Analyzing your database schema...',\n    detail: 'Step 1 of 3',\n    theme: 'light',\n  },\n  render: (args) => html`\n    <div style=\"width: 500px; padding: 20px; background: ${args.theme === 'light' ? '#ffffff' : 'rgb(11, 15, 25)'};\">\n      <vanna-status-bar\n        .status=${args.status}\n        .message=${args.message}\n        .detail=${args.detail}\n        theme=${args.theme}>\n      </vanna-status-bar>\n    </div>\n  `,\n};\n\nexport const Success: Story = {\n  args: {\n    status: 'success',\n    message: 'Query executed successfully',\n    detail: '2.3s',\n  },\n  render: (args) => html`\n    <div style=\"width: 500px; padding: 20px;\">\n      <vanna-status-bar\n        .status=${args.status}\n        .message=${args.message}\n        .detail=${args.detail}>\n      </vanna-status-bar>\n    </div>\n  `,\n};\n\nexport const Error: Story = {\n  args: {\n    status: 'error',\n    message: 'Failed to connect to database',\n    detail: 'Connection timeout after 30s',\n  },\n  render: (args) => html`\n    <div style=\"width: 500px; padding: 20px;\">\n      <vanna-status-bar\n        .status=${args.status}\n        .message=${args.message}\n        .detail=${args.detail}>\n      </vanna-status-bar>\n    </div>\n  `,\n};\n\nexport const StatusSequence: Story = {\n  render: () => {\n    let statusBar: any;\n    let currentIndex = 0;\n    const statuses = [\n      { status: 'working', message: 'Starting analysis...', detail: 'Initializing' },\n      { status: 'working', message: 'Querying database...', detail: 'Step 1 of 3' },\n      { status: 'working', message: 'Processing results...', detail: 'Step 2 of 3' },\n      { status: 'working', message: 'Generating visualization...', detail: 'Step 3 of 3' },\n      { status: 'success', message: 'Analysis complete!', detail: '4.2s total' },\n    ];\n\n    const updateStatus = () => {\n      if (statusBar && currentIndex < statuses.length) {\n        const current = statuses[currentIndex];\n        statusBar.status = current.status;\n        statusBar.message = current.message;\n        statusBar.detail = current.detail;\n        currentIndex++;\n\n        if (currentIndex < statuses.length) {\n          setTimeout(updateStatus, 2000);\n        }\n      }\n    };\n\n    setTimeout(() => {\n      statusBar = document.querySelector('vanna-status-bar');\n      updateStatus();\n    }, 100);\n\n    return html`\n      <div style=\"width: 500px; padding: 20px;\">\n        <vanna-status-bar status=\"idle\"></vanna-status-bar>\n        <div style=\"margin-top: 10px; color: #999; font-size: 12px;\">\n          Watch the status bar cycle through different states\n        </div>\n      </div>\n    `;\n  },\n};"
  },
  {
    "path": "frontends/webcomponent/src/components/vanna-status-bar.ts",
    "content": "import { LitElement, html, css } from 'lit';\nimport { customElement, property } from 'lit/decorators.js';\nimport { vannaDesignTokens } from '../styles/vanna-design-tokens.js';\n\n@customElement('vanna-status-bar')\nexport class VannaStatusBar extends LitElement {\n  static styles = [\n    vannaDesignTokens,\n    css`\n      :host {\n        display: block;\n        background: rgba(254, 93, 38, 0.1);\n        border: 2px solid var(--vanna-orange);\n        border-radius: var(--vanna-border-radius-xl);\n        padding: var(--vanna-space-4) var(--vanna-space-5);\n        margin-bottom: var(--vanna-space-4);\n        font-family: var(--vanna-font-family-default);\n        font-size: 14px;\n        font-weight: 500;\n        color: var(--vanna-navy);\n        backdrop-filter: blur(12px);\n        box-shadow:\n          var(--vanna-shadow-lg),\n          0 0 0 1px rgba(254, 93, 38, 0.1);\n        \n        /* Animation properties */\n        opacity: 1;\n        transform: translateY(0) scale(1);\n        max-height: 200px;\n        overflow: hidden;\n        transition: \n          opacity var(--vanna-duration-300) cubic-bezier(0.4, 0, 0.2, 1),\n          transform var(--vanna-duration-300) cubic-bezier(0.4, 0, 0.2, 1),\n          max-height var(--vanna-duration-300) ease,\n          margin var(--vanna-duration-300) ease,\n          padding var(--vanna-duration-300) ease,\n          box-shadow var(--vanna-duration-200) ease;\n      }\n\n      /* Hide when there's no actual content */\n      :host(.no-content) {\n        opacity: 0;\n        transform: translateY(-8px) scale(0.95);\n        max-height: 0;\n        margin: 0;\n        padding: 0;\n        pointer-events: none;\n      }\n\n      :host(:empty) {\n        display: none;\n      }\n\n      /* Entrance animation when content appears */\n      :host(.entering) {\n        animation: statusEnter var(--vanna-duration-300) ease-out;\n      }\n\n      /* Exit animation when content disappears */\n      :host(.exiting) {\n        animation: statusExit var(--vanna-duration-300) ease-in;\n      }\n\n      @keyframes statusEnter {\n        0% {\n          opacity: 0;\n          transform: translateY(-12px) scale(0.9);\n          max-height: 0;\n        }\n        50% {\n          opacity: 0.8;\n          transform: translateY(-2px) scale(1.02);\n        }\n        100% {\n          opacity: 1;\n          transform: translateY(0) scale(1);\n          max-height: 200px;\n        }\n      }\n\n      @keyframes statusExit {\n        0% {\n          opacity: 1;\n          transform: translateY(0) scale(1);\n          max-height: 200px;\n        }\n        50% {\n          opacity: 0.5;\n          transform: translateY(-4px) scale(0.98);\n        }\n        100% {\n          opacity: 0;\n          transform: translateY(-12px) scale(0.9);\n          max-height: 0;\n        }\n      }\n\n      :host([status=\"working\"]) {\n        background: var(--vanna-orange);\n        border-color: var(--vanna-orange);\n        color: white;\n        box-shadow:\n          var(--vanna-shadow-xl),\n          0 0 0 2px rgba(254, 93, 38, 0.3),\n          0 0 20px rgba(254, 93, 38, 0.4);\n      }\n\n      :host([status=\"error\"]) {\n        background: linear-gradient(135deg, var(--vanna-accent-negative-subtle) 0%, rgba(239, 68, 68, 0.15) 100%);\n        border-color: var(--vanna-accent-negative-default);\n        color: var(--vanna-accent-negative-stronger);\n        box-shadow: \n          var(--vanna-shadow-xl),\n          0 0 0 2px rgba(239, 68, 68, 0.3),\n          0 0 20px rgba(239, 68, 68, 0.2);\n        animation: errorShake 0.5s ease-in-out, errorGlow 2s ease-in-out;\n      }\n\n      :host([status=\"success\"]) {\n        background: linear-gradient(135deg, var(--vanna-accent-positive-subtle) 0%, rgba(16, 185, 129, 0.15) 100%);\n        border-color: var(--vanna-accent-positive-default);\n        color: var(--vanna-accent-positive-stronger);\n        box-shadow: \n          var(--vanna-shadow-xl),\n          0 0 0 2px rgba(16, 185, 129, 0.3),\n          0 0 20px rgba(16, 185, 129, 0.2);\n        animation: successPulse 0.6s ease-out, successGlow 2s ease-out;\n      }\n\n      @keyframes errorShake {\n        0%, 100% { transform: translateX(0); }\n        10%, 30%, 50%, 70%, 90% { transform: translateX(-4px); }\n        20%, 40%, 60%, 80% { transform: translateX(4px); }\n      }\n\n      @keyframes successPulse {\n        0% { \n          transform: scale(1); \n        }\n        50% { \n          transform: scale(1.05); \n        }\n        100% { \n          transform: scale(1); \n        }\n      }\n\n      .status-content {\n        display: flex;\n        align-items: center;\n        gap: var(--vanna-space-3);\n        animation: contentFadeIn var(--vanna-duration-200) ease-out;\n      }\n\n      @keyframes contentFadeIn {\n        0% {\n          opacity: 0;\n          transform: translateY(4px);\n        }\n        100% {\n          opacity: 1;\n          transform: translateY(0);\n        }\n      }\n\n      .status-indicator {\n        width: 12px;\n        height: 12px;\n        border-radius: var(--vanna-border-radius-full);\n        background: var(--vanna-accent-primary-default);\n        flex-shrink: 0;\n        box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5), 0 2px 8px rgba(0, 0, 0, 0.15);\n      }\n\n      .status-indicator.working {\n        background: white;\n        animation: workingPulse 1.5s ease-in-out infinite;\n      }\n\n      .status-indicator.error {\n        background: linear-gradient(45deg, var(--vanna-accent-negative-default), var(--vanna-accent-negative-stronger));\n        box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5), 0 0 8px rgba(239, 68, 68, 0.4);\n      }\n\n      .status-indicator.success {\n        background: linear-gradient(45deg, var(--vanna-accent-positive-default), var(--vanna-accent-positive-stronger));\n        box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5), 0 0 8px rgba(16, 185, 129, 0.4);\n      }\n\n      .spinner {\n        width: 16px;\n        height: 16px;\n        border: 3px solid rgba(21, 168, 168, 0.3);\n        border-top-color: var(--vanna-teal);\n        border-radius: var(--vanna-border-radius-full);\n        animation: spin 1s linear infinite, spinnerGlow 2s ease-in-out infinite;\n        flex-shrink: 0;\n      }\n\n      .status-text {\n        flex: 1;\n        font-weight: 600;\n        line-height: 1.4;\n        letter-spacing: 0.01em;\n      }\n\n      .status-detail {\n        font-size: 12px;\n        color: var(--vanna-foreground-dimmest);\n        margin-left: var(--vanna-space-4);\n        opacity: 0.9;\n        font-weight: 500;\n      }\n\n      .status-actions {\n        display: flex;\n        align-items: center;\n        gap: var(--vanna-space-2);\n        margin-left: auto;\n      }\n\n      .status-button {\n        padding: var(--vanna-space-1) var(--vanna-space-2);\n        border: 1px solid var(--vanna-outline-default);\n        border-radius: var(--vanna-border-radius-sm);\n        background: var(--vanna-background-subtle);\n        color: var(--vanna-foreground-dimmer);\n        font-size: 11px;\n        font-weight: 500;\n        cursor: pointer;\n        transition: all var(--vanna-duration-150) ease;\n      }\n\n      .status-button:hover {\n        background: var(--vanna-background-higher);\n        border-color: var(--vanna-outline-hover);\n        color: var(--vanna-foreground-default);\n      }\n\n      @keyframes spin {\n        to {\n          transform: rotate(360deg);\n        }\n      }\n\n      @keyframes pulse {\n        0%, 100% {\n          opacity: 1;\n          transform: scale(1);\n        }\n        50% {\n          opacity: 0.6;\n          transform: scale(1.1);\n        }\n      }\n\n      @keyframes workingPulse {\n        0%, 100% {\n          opacity: 1;\n          transform: scale(1);\n          box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.8), 0 2px 8px rgba(255, 255, 255, 0.3);\n        }\n        50% {\n          opacity: 0.9;\n          transform: scale(1.2);\n          box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.9), 0 4px 12px rgba(255, 255, 255, 0.5);\n        }\n      }\n\n      @keyframes spinnerGlow {\n        0%, 100% {\n          filter: drop-shadow(0 0 2px rgba(21, 168, 168, 0.5));\n        }\n        50% {\n          filter: drop-shadow(0 0 6px rgba(21, 168, 168, 0.8));\n        }\n      }\n\n      @keyframes errorGlow {\n        0% {\n          box-shadow: \n            var(--vanna-shadow-xl),\n            0 0 0 2px rgba(239, 68, 68, 0.3),\n            0 0 20px rgba(239, 68, 68, 0.2);\n        }\n        50% {\n          box-shadow: \n            var(--vanna-shadow-2xl),\n            0 0 0 3px rgba(239, 68, 68, 0.4),\n            0 0 30px rgba(239, 68, 68, 0.3);\n        }\n        100% {\n          box-shadow: \n            var(--vanna-shadow-xl),\n            0 0 0 2px rgba(239, 68, 68, 0.3),\n            0 0 20px rgba(239, 68, 68, 0.2);\n        }\n      }\n\n      @keyframes successGlow {\n        0% {\n          box-shadow: \n            var(--vanna-shadow-xl),\n            0 0 0 2px rgba(16, 185, 129, 0.3),\n            0 0 20px rgba(16, 185, 129, 0.2);\n        }\n        50% {\n          box-shadow: \n            var(--vanna-shadow-2xl),\n            0 0 0 3px rgba(16, 185, 129, 0.4),\n            0 0 30px rgba(16, 185, 129, 0.3);\n        }\n        100% {\n          box-shadow: \n            var(--vanna-shadow-xl),\n            0 0 0 2px rgba(16, 185, 129, 0.3),\n            0 0 20px rgba(16, 185, 129, 0.2);\n        }\n      }\n\n      /* Dark theme overrides */\n      :host([theme=\"dark\"]) {\n        background: var(--vanna-background-higher);\n        border-color: var(--vanna-outline-default);\n      }\n\n      :host([theme=\"dark\"]) .status-button {\n        background: var(--vanna-background-highest);\n        border-color: var(--vanna-outline-default);\n      }\n\n      :host([theme=\"dark\"]) .status-button:hover {\n        background: var(--vanna-background-highest);\n        border-color: var(--vanna-outline-hover);\n      }\n    `\n  ];\n\n  @property() status: 'idle' | 'working' | 'error' | 'success' = 'idle';\n  @property() message = '';\n  @property() detail = '';\n  @property() theme = 'light';\n\n  private _previousHasContent = false;\n  private _enterTimeout: number | null = null;\n  private _exitTimeout: number | null = null;\n  private _lastUpdateTime = 0;\n\n  disconnectedCallback() {\n    super.disconnectedCallback();\n\n    // Clean up pending animation timeouts when component is removed\n    if (this._enterTimeout !== null) {\n      clearTimeout(this._enterTimeout);\n      this._enterTimeout = null;\n    }\n    if (this._exitTimeout !== null) {\n      clearTimeout(this._exitTimeout);\n      this._exitTimeout = null;\n    }\n  }\n\n  updated(_changedProperties: Map<string | number | symbol, unknown>) {\n    // Update CSS class based on content\n    const hasContent = Boolean(this.message && this.message.trim());\n\n    // Cancel any pending animation timeouts to prevent race conditions\n    if (this._enterTimeout !== null) {\n      clearTimeout(this._enterTimeout);\n      this._enterTimeout = null;\n    }\n    if (this._exitTimeout !== null) {\n      clearTimeout(this._exitTimeout);\n      this._exitTimeout = null;\n    }\n\n    // Debounce rapid updates to prevent animation jank\n    const now = Date.now();\n    const timeSinceLastUpdate = now - this._lastUpdateTime;\n    const shouldDebounce = timeSinceLastUpdate < 100; // 100ms debounce\n\n    // Handle animation classes\n    if (hasContent !== this._previousHasContent) {\n      if (hasContent) {\n        // Content appeared - animate in\n        this.classList.remove('no-content', 'exiting');\n\n        if (!shouldDebounce) {\n          // Only animate if not rapid-firing\n          this.classList.add('entering');\n\n          // Remove entering class after animation\n          this._enterTimeout = window.setTimeout(() => {\n            this.classList.remove('entering');\n            this._enterTimeout = null;\n          }, 300);\n        }\n      } else {\n        // Content disappeared - animate out\n        this.classList.remove('entering');\n\n        if (!shouldDebounce) {\n          // Only animate if not rapid-firing\n          this.classList.add('exiting');\n\n          // Add no-content class after animation\n          this._exitTimeout = window.setTimeout(() => {\n            this.classList.remove('exiting');\n            this.classList.add('no-content');\n            this._exitTimeout = null;\n          }, 300);\n        } else {\n          // If rapid-firing, skip animation and go straight to no-content\n          this.classList.add('no-content');\n        }\n      }\n    } else if (!hasContent) {\n      // Ensure no-content class is applied when no content\n      this.classList.add('no-content');\n    }\n\n    this._previousHasContent = hasContent;\n    this._lastUpdateTime = now;\n  }\n\n  render() {\n    // Only show if there's actual content (message) to display\n    if (!this.message || !this.message.trim()) {\n      return html``;\n    }\n\n    return html`\n      <div class=\"status-content\">\n        ${this.status === 'working'\n          ? html`<div class=\"spinner\"></div>`\n          : html`<div class=\"status-indicator ${this.status}\"></div>`\n        }\n        <span class=\"status-text\">${this.message}</span>\n        ${this.detail ? html`<span class=\"status-detail\">${this.detail}</span>` : ''}\n      </div>\n    `;\n  }\n}"
  },
  {
    "path": "frontends/webcomponent/src/index.ts",
    "content": "// Log build information when the module loads\nconsole.log(\n  '%c🎨 Vanna Web Components',\n  'color: #4CAF50; font-weight: bold; font-size: 14px;'\n);\nconsole.log(\n  `%c📦 Version: ${__BUILD_VERSION__}`,\n  'color: #2196F3; font-weight: bold;'\n);\nconsole.log(\n  `%c🕐 Built: ${__BUILD_TIME__}`,\n  'color: #FF9800; font-weight: bold;'\n);\nconsole.log(\n  '%c━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',\n  'color: #9E9E9E;'\n);\n\nexport { VannaChat } from './components/vanna-chat';\nexport { VannaMessage } from './components/vanna-message';\nexport { VannaStatusBar } from './components/vanna-status-bar';\nexport { VannaProgressTracker } from './components/vanna-progress-tracker';\nexport { PlotlyChart } from './components/plotly-chart';\n\n// Rich component system\nexport {\n  ComponentRegistry,\n  ComponentManager,\n  CardComponentRenderer,\n  TaskListComponentRenderer,\n  ProgressBarComponentRenderer,\n  NotificationComponentRenderer,\n  StatusIndicatorComponentRenderer,\n  TextComponentRenderer\n} from './components/rich-component-system';\n\n// Rich component styles are injected automatically by the ComponentManager\n"
  },
  {
    "path": "frontends/webcomponent/src/services/api-client.ts",
    "content": "/**\n * API client for communicating with Vanna Agents backend\n */\n\nexport interface ChatMessage {\n  id: string;\n  content: string;\n  type: 'user' | 'assistant';\n  timestamp: number;\n}\n\nexport interface ChatRequest {\n  message: string;\n  conversation_id?: string;\n  user_id?: string;\n  request_id?: string;\n  metadata?: Record<string, any>;\n}\n\nexport interface ChatStreamChunk {\n  rich: Record<string, any>;\n  simple?: Record<string, any>;\n  conversation_id: string;\n  request_id: string;\n  timestamp: number;\n}\n\nexport interface ChatResponse {\n  chunks: ChatStreamChunk[];\n  conversation_id: string;\n  request_id: string;\n  total_chunks: number;\n}\n\nexport interface ApiClientConfig {\n  baseUrl?: string;\n  sseEndpoint?: string;\n  wsEndpoint?: string;\n  pollEndpoint?: string;\n  timeout?: number;\n  customHeaders?: Record<string, string>;\n}\n\nexport class VannaApiClient {\n  public readonly baseUrl: string;\n  private sseEndpoint: string;\n  private wsEndpoint: string;\n  private pollEndpoint: string;\n  private timeout: number;\n  private customHeaders: Record<string, string>;\n\n  constructor(config: ApiClientConfig = {}) {\n    this.baseUrl = config.baseUrl || '';\n    this.sseEndpoint = config.sseEndpoint || '/api/vanna/v2/chat_sse';\n    this.wsEndpoint = config.wsEndpoint || '/api/vanna/v2/chat_websocket';\n    this.pollEndpoint = config.pollEndpoint || '/api/vanna/v2/chat_poll';\n    this.timeout = config.timeout || 30000;\n    this.customHeaders = config.customHeaders || {};\n\n    console.log('[VannaApiClient] Constructor called with config:', config);\n    console.log('[VannaApiClient] Endpoint configuration:');\n    console.log('  - SSE endpoint:', this.sseEndpoint, config.sseEndpoint ? '(custom)' : '(default)');\n    console.log('  - WS endpoint:', this.wsEndpoint, config.wsEndpoint ? '(custom)' : '(default)');\n    console.log('  - Poll endpoint:', this.pollEndpoint, config.pollEndpoint ? '(custom)' : '(default)');\n    console.log('  - Base URL:', this.baseUrl || '(empty)');\n  }\n\n  /**\n   * Update custom headers (e.g., for authentication)\n   */\n  setCustomHeaders(headers: Record<string, string>) {\n    this.customHeaders = headers;\n  }\n\n  /**\n   * Get current custom headers\n   */\n  getCustomHeaders(): Record<string, string> {\n    return { ...this.customHeaders };\n  }\n\n  /**\n   * Send message using Server-Sent Events (SSE) streaming\n   */\n  async *streamChat(request: ChatRequest): AsyncGenerator<ChatStreamChunk, void, unknown> {\n    const url = this.sseEndpoint.startsWith('http')\n      ? this.sseEndpoint\n      : `${this.baseUrl}${this.sseEndpoint}`;\n\n    console.log('[VannaApiClient] SSE streaming to URL:', url);\n    console.log('[VannaApiClient] SSE endpoint config:', {\n      baseUrl: this.baseUrl,\n      sseEndpoint: this.sseEndpoint,\n      constructedUrl: url\n    });\n\n    const response = await fetch(url, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'Accept': 'text/event-stream',\n        ...this.customHeaders,\n      },\n      body: JSON.stringify(request),\n    });\n\n    if (!response.ok) {\n      throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n    }\n\n    const reader = response.body?.getReader();\n    if (!reader) {\n      throw new Error('No response body');\n    }\n\n    const decoder = new TextDecoder();\n    let buffer = '';\n\n    try {\n      while (true) {\n        const { done, value } = await reader.read();\n        if (done) break;\n\n        buffer += decoder.decode(value, { stream: true });\n        const lines = buffer.split('\\n');\n        buffer = lines.pop() || '';\n\n        for (const line of lines) {\n          if (line.startsWith('data: ')) {\n            const data = line.slice(6).trim();\n            if (data === '[DONE]') {\n              return;\n            }\n\n            try {\n              const chunk = JSON.parse(data) as ChatStreamChunk;\n              yield chunk;\n            } catch (e) {\n              console.warn('Failed to parse SSE chunk:', data, e);\n            }\n          }\n        }\n      }\n    } finally {\n      reader.releaseLock();\n    }\n  }\n\n  /**\n   * Send message using WebSocket\n   */\n  createWebSocketConnection(): Promise<WebSocket> {\n    return new Promise((resolve, reject) => {\n      let wsUrl: string;\n\n      if (this.wsEndpoint.startsWith('ws://') || this.wsEndpoint.startsWith('wss://')) {\n        // Absolute WebSocket URL provided\n        wsUrl = this.wsEndpoint;\n      } else {\n        // Relative path - construct from baseUrl\n        if (this.baseUrl) {\n          // Parse baseUrl to extract host and convert http(s) to ws(s)\n          const baseUrlObj = new URL(this.baseUrl);\n          const wsProtocol = baseUrlObj.protocol === 'https:' ? 'wss:' : 'ws:';\n          wsUrl = `${wsProtocol}//${baseUrlObj.host}${this.wsEndpoint}`;\n        } else {\n          // Fallback to window.location\n          const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n          wsUrl = `${protocol}//${window.location.host}${this.wsEndpoint}`;\n        }\n      }\n\n      const ws = new WebSocket(wsUrl);\n\n      ws.onopen = () => resolve(ws);\n      ws.onerror = (error) => reject(error);\n\n      // Set timeout\n      setTimeout(() => {\n        if (ws.readyState === WebSocket.CONNECTING) {\n          ws.close();\n          reject(new Error('WebSocket connection timeout'));\n        }\n      }, this.timeout);\n    });\n  }\n\n  /**\n   * Send message via WebSocket\n   */\n  async sendWebSocketMessage(\n    ws: WebSocket,\n    request: ChatRequest\n  ): Promise<AsyncGenerator<ChatStreamChunk, void, unknown>> {\n    return new Promise((resolve, reject) => {\n      if (ws.readyState !== WebSocket.OPEN) {\n        reject(new Error('WebSocket not connected'));\n        return;\n      }\n\n      async function* generator() {\n        let isCompleted = false;\n        const messageQueue: ChatStreamChunk[] = [];\n        let resolveNext: ((value: IteratorResult<ChatStreamChunk>) => void) | null = null;\n\n        const messageHandler = (event: MessageEvent) => {\n          try {\n            const chunk = JSON.parse(event.data) as ChatStreamChunk;\n\n            if (chunk.rich?.type === 'completion') {\n              isCompleted = true;\n              if (resolveNext) {\n                resolveNext({ done: true, value: undefined });\n                resolveNext = null;\n              }\n              return;\n            }\n\n            if (chunk.rich?.type === 'error') {\n              ws.removeEventListener('message', messageHandler);\n              if (resolveNext) {\n                resolveNext({ done: true, value: undefined });\n              }\n              return;\n            }\n\n            if (resolveNext) {\n              resolveNext({ done: false, value: chunk });\n              resolveNext = null;\n            } else {\n              messageQueue.push(chunk);\n            }\n          } catch (e) {\n            console.warn('Failed to parse WebSocket message:', event.data, e);\n          }\n        };\n\n        ws.addEventListener('message', messageHandler);\n\n        while (!isCompleted) {\n          if (messageQueue.length > 0) {\n            yield messageQueue.shift()!;\n          } else {\n            await new Promise<IteratorResult<ChatStreamChunk>>((resolve) => {\n              resolveNext = resolve;\n            });\n          }\n        }\n\n        ws.removeEventListener('message', messageHandler);\n      }\n\n      try {\n        ws.send(JSON.stringify(request));\n        resolve(generator());\n      } catch (error) {\n        reject(error);\n      }\n    });\n  }\n\n  /**\n   * Send message using polling (fallback option)\n   */\n  async sendPollMessage(request: ChatRequest): Promise<ChatResponse> {\n    const url = this.pollEndpoint.startsWith('http')\n      ? this.pollEndpoint\n      : `${this.baseUrl}${this.pollEndpoint}`;\n    const response = await fetch(url, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        ...this.customHeaders,\n      },\n      body: JSON.stringify(request),\n    });\n\n    if (!response.ok) {\n      throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n    }\n\n    return response.json() as Promise<ChatResponse>;\n  }\n\n  /**\n   * Generate unique IDs for conversations and requests\n   */\n  generateId(): string {\n    return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;\n  }\n}\n\n/**\n * Default API client instance\n */\nexport const apiClient = new VannaApiClient();"
  },
  {
    "path": "frontends/webcomponent/src/styles/rich-component-styles.ts",
    "content": "import { css } from 'lit';\n\nexport const richComponentStyles = css`\n  .rich-component {\n    margin-bottom: var(--vanna-space-4);\n    border-radius: var(--vanna-border-radius-lg);\n    background: var(--vanna-background-default);\n    border: 1px solid var(--vanna-outline-default);\n    box-shadow: var(--vanna-shadow-sm);\n    transition: box-shadow var(--vanna-duration-200) ease;\n    font-family: var(--vanna-font-family-default);\n  }\n\n  .rich-component:hover {\n    box-shadow: var(--vanna-shadow-md);\n  }\n\n  /* Shared typography */\n  .rich-component h3,\n  .rich-component h4 {\n    margin: 0;\n    color: var(--vanna-foreground-default);\n    font-weight: 600;\n  }\n\n  .rich-component p,\n  .rich-component span,\n  .rich-component div {\n    color: var(--vanna-foreground-default);\n  }\n\n  /* Card */\n  .rich-card {\n    overflow: hidden;\n  }\n\n  .card-header {\n    display: flex;\n    align-items: center;\n    gap: var(--vanna-space-3);\n    padding: var(--vanna-space-4) var(--vanna-space-5);\n    background: var(--vanna-background-higher);\n    border-bottom: 1px solid var(--vanna-outline-default);\n  }\n\n  .card-header.collapsible {\n    cursor: pointer;\n  }\n\n  .card-icon {\n    font-size: 1.25rem;\n    display: flex;\n  }\n\n  .card-title-section {\n    flex: 1;\n  }\n\n  .card-title {\n    margin: 0;\n    font-size: 1rem;\n    color: var(--vanna-foreground-default);\n  }\n\n  .card-subtitle {\n    margin: var(--vanna-space-1) 0 0 0;\n    font-size: 0.875rem;\n    color: var(--vanna-foreground-dimmer);\n  }\n\n  .card-status {\n    padding: var(--vanna-space-1) var(--vanna-space-2);\n    border-radius: var(--vanna-border-radius-md);\n    font-size: 0.75rem;\n    font-weight: 600;\n    text-transform: uppercase;\n    background: rgba(0, 123, 255, 0.15);\n    color: var(--vanna-accent-primary-default);\n  }\n\n  .card-status.status-success {\n    background: rgba(16, 185, 129, 0.15);\n    color: var(--vanna-accent-positive-default);\n  }\n\n  .card-status.status-warning {\n    background: rgba(245, 158, 11, 0.15);\n    color: var(--vanna-accent-warning-default);\n  }\n\n  .card-status.status-error {\n    background: rgba(239, 68, 68, 0.15);\n    color: var(--vanna-accent-negative-default);\n  }\n\n  .card-toggle {\n    margin-left: var(--vanna-space-2);\n    border: none;\n    background: none;\n    cursor: pointer;\n    color: var(--vanna-foreground-dimmer);\n    font-size: 1rem;\n    padding: var(--vanna-space-1);\n    border-radius: var(--vanna-border-radius-sm);\n    transition: background-color var(--vanna-duration-200) ease;\n  }\n\n  .card-toggle:hover {\n    background: var(--vanna-background-root);\n  }\n\n  .card-content {\n    padding: var(--vanna-space-4) var(--vanna-space-5);\n    line-height: 1.6;\n    transition: all var(--vanna-duration-200) ease;\n  }\n\n  .card-content.collapsed {\n    max-height: 0;\n    padding-top: 0;\n    padding-bottom: 0;\n    overflow: hidden;\n  }\n\n  .card-actions {\n    padding: var(--vanna-space-3) var(--vanna-space-5);\n    background: var(--vanna-background-root);\n    border-top: 1px solid var(--vanna-outline-default);\n    display: flex;\n    gap: var(--vanna-space-2);\n  }\n\n  .card-action {\n    padding: var(--vanna-space-2) var(--vanna-space-4);\n    border-radius: var(--vanna-border-radius-md);\n    border: 1px solid var(--vanna-outline-default);\n    background: var(--vanna-background-default);\n    color: var(--vanna-foreground-default);\n    cursor: pointer;\n    font-size: 0.875rem;\n    font-weight: 500;\n    transition: all var(--vanna-duration-200) ease;\n  }\n\n  .card-action:hover {\n    background: var(--vanna-background-higher);\n  }\n\n  .card-action.primary {\n    background: var(--vanna-accent-primary-default);\n    border-color: var(--vanna-accent-primary-default);\n    color: white;\n  }\n\n  .card-action.primary:hover {\n    background: var(--vanna-accent-primary-stronger);\n  }\n\n  /* Task list */\n  .rich-task-list {\n    padding-bottom: var(--vanna-space-2);\n  }\n\n  .task-list-header {\n    padding: var(--vanna-space-4) var(--vanna-space-5);\n    background: var(--vanna-background-higher);\n    border-bottom: 1px solid var(--vanna-outline-default);\n  }\n\n  .task-list-title {\n    margin-bottom: var(--vanna-space-3);\n    font-size: 1rem;\n  }\n\n  .task-list-progress {\n    display: flex;\n    align-items: center;\n    gap: var(--vanna-space-3);\n  }\n\n  .task-list-progress .progress-text {\n    font-size: 0.875rem;\n    color: var(--vanna-foreground-dimmer);\n    min-width: fit-content;\n  }\n\n  .task-list-progress .progress-bar {\n    flex: 1;\n    height: 6px;\n    background: var(--vanna-background-root);\n    border-radius: 3px;\n    overflow: hidden;\n  }\n\n  .task-list-progress .progress-fill {\n    height: 100%;\n    background: var(--vanna-accent-primary-default);\n    border-radius: 3px;\n    transition: width var(--vanna-duration-300) ease;\n  }\n\n  .task-list-items {\n    padding: var(--vanna-space-4) var(--vanna-space-5);\n    display: flex;\n    flex-direction: column;\n    gap: var(--vanna-space-3);\n  }\n\n  .task-item {\n    display: flex;\n    gap: var(--vanna-space-3);\n    padding: var(--vanna-space-3);\n    border-radius: var(--vanna-border-radius-md);\n    background: var(--vanna-background-default);\n    border: 1px solid var(--vanna-outline-dimmer);\n  }\n\n  .task-item.status-running {\n    border-color: var(--vanna-accent-primary-default);\n    box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.2);\n  }\n\n  .task-item.status-completed {\n    opacity: 0.85;\n  }\n\n  .task-icon {\n    font-size: 1.2rem;\n    margin-top: 2px;\n  }\n\n  .task-title {\n    margin: 0;\n    font-size: 0.95rem;\n  }\n\n  .task-description {\n    margin: var(--vanna-space-1) 0 0 0;\n    font-size: 0.85rem;\n    color: var(--vanna-foreground-dimmer);\n  }\n\n  .task-progress {\n    display: flex;\n    gap: var(--vanna-space-2);\n    align-items: center;\n    margin-top: var(--vanna-space-2);\n  }\n\n  .task-progress-bar {\n    flex: 1;\n    height: 6px;\n    background: var(--vanna-background-root);\n    border-radius: 3px;\n    overflow: hidden;\n  }\n\n  .task-progress-fill {\n    height: 100%;\n    background: var(--vanna-accent-primary-default);\n    transition: width var(--vanna-duration-300) ease;\n  }\n\n  .task-progress-text {\n    font-size: 0.75rem;\n    color: var(--vanna-foreground-dimmer);\n  }\n\n  .task-timestamp {\n    margin-top: var(--vanna-space-2);\n    font-size: 0.75rem;\n    color: var(--vanna-foreground-dimmest);\n    font-variant-numeric: tabular-nums;\n  }\n\n  /* Tool execution */\n  .rich-tool-execution {\n    padding: var(--vanna-space-4) var(--vanna-space-5);\n  }\n\n  .tool-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    margin-bottom: var(--vanna-space-3);\n  }\n\n  .tool-status {\n    display: flex;\n    align-items: center;\n    gap: var(--vanna-space-2);\n  }\n\n  .tool-icon {\n    font-size: 1.2rem;\n  }\n\n  .tool-name {\n    font-weight: 600;\n  }\n\n  .status-badge {\n    padding: 2px 8px;\n    border-radius: var(--vanna-border-radius-sm);\n    font-size: 0.75rem;\n    text-transform: uppercase;\n    background: rgba(0, 123, 255, 0.15);\n    color: var(--vanna-accent-primary-default);\n  }\n\n  .status-badge.status-completed {\n    background: rgba(16, 185, 129, 0.15);\n    color: var(--vanna-accent-positive-default);\n  }\n\n  .status-badge.status-failed {\n    background: rgba(239, 68, 68, 0.15);\n    color: var(--vanna-accent-negative-default);\n  }\n\n  .tool-duration {\n    font-size: 0.85rem;\n    color: var(--vanna-foreground-dimmest);\n  }\n\n  .tool-progress {\n    display: flex;\n    align-items: center;\n    gap: var(--vanna-space-3);\n    margin-bottom: var(--vanna-space-3);\n  }\n\n  .tool-progress .progress-bar {\n    flex: 1;\n    height: 8px;\n    background: var(--vanna-background-root);\n    border-radius: 4px;\n    overflow: hidden;\n  }\n\n  .tool-progress .progress-fill {\n    height: 100%;\n    background: var(--vanna-accent-primary-default);\n    transition: width var(--vanna-duration-300) ease;\n  }\n\n  .tool-progress .progress-text {\n    font-size: 0.8rem;\n    color: var(--vanna-foreground-dimmer);\n  }\n\n  .tool-section {\n    margin-top: var(--vanna-space-4);\n  }\n\n  .tool-section h4 {\n    margin-bottom: var(--vanna-space-2);\n    font-size: 0.9rem;\n  }\n\n  .tool-arguments,\n  .tool-result,\n  .tool-error {\n    background: var(--vanna-background-root);\n    border: 1px solid var(--vanna-outline-dimmer);\n    border-radius: var(--vanna-border-radius-md);\n    padding: var(--vanna-space-3);\n    font-family: var(--vanna-font-family-mono);\n    font-size: 0.85rem;\n    line-height: 1.5;\n    white-space: pre-wrap;\n    color: var(--vanna-foreground-default);\n  }\n\n  .tool-section.error .tool-error {\n    border-color: var(--vanna-accent-negative-default);\n    background: rgba(239, 68, 68, 0.1);\n  }\n\n  .tool-logs {\n    display: flex;\n    flex-direction: column;\n    gap: var(--vanna-space-2);\n    max-height: 200px;\n    overflow-y: auto;\n    padding-right: 4px;\n  }\n\n  .log-entry {\n    display: flex;\n    gap: var(--vanna-space-2);\n    font-size: 0.85rem;\n    color: var(--vanna-foreground-default);\n  }\n\n  .log-entry .log-timestamp {\n    font-family: var(--vanna-font-family-mono);\n    color: var(--vanna-foreground-dimmest);\n    min-width: 110px;\n  }\n\n  .log-entry .log-level {\n    color: var(--vanna-foreground-dimmer);\n  }\n\n  .log-entry.log-error .log-level {\n    color: var(--vanna-accent-negative-default);\n  }\n\n  .log-entry.log-warning .log-level {\n    color: var(--vanna-accent-warning-default);\n  }\n\n  /* Progress bar */\n  .rich-progress-bar {\n    padding: var(--vanna-space-4) var(--vanna-space-5);\n  }\n\n  .progress-header {\n    display: flex;\n    justify-content: space-between;\n    font-size: 0.85rem;\n    margin-bottom: var(--vanna-space-3);\n    color: var(--vanna-foreground-dimmer);\n  }\n\n  .progress-track {\n    position: relative;\n    height: 10px;\n    background: var(--vanna-background-root);\n    border-radius: 5px;\n    overflow: hidden;\n  }\n\n  .progress-fill {\n    height: 100%;\n    background: var(--vanna-accent-primary-default);\n    transition: width var(--vanna-duration-300) ease;\n  }\n\n  .progress-fill.animated {\n    animation: progressPulse 2s ease-in-out infinite;\n  }\n\n  .progress-fill.status-success {\n    background: var(--vanna-accent-positive-default);\n  }\n\n  .progress-fill.status-error {\n    background: var(--vanna-accent-negative-default);\n  }\n\n  .progress-fill.status-warning {\n    background: var(--vanna-accent-warning-default);\n  }\n\n  @keyframes progressPulse {\n    0%, 100% { opacity: 1; }\n    50% { opacity: 0.6; }\n  }\n\n  /* Notifications */\n  .rich-notification {\n    padding: var(--vanna-space-4) var(--vanna-space-5);\n    font-family: var(--vanna-font-family-default);\n  }\n\n  .notification-content {\n    display: flex;\n    gap: var(--vanna-space-3);\n    align-items: flex-start;\n    position: relative;\n  }\n\n  .notification-content.level-info {\n    border-left: 4px solid var(--vanna-accent-primary-default);\n    padding-left: var(--vanna-space-3);\n  }\n\n  .notification-content.level-success {\n    border-left: 4px solid var(--vanna-accent-positive-default);\n    padding-left: var(--vanna-space-3);\n  }\n\n  .notification-content.level-warning {\n    border-left: 4px solid var(--vanna-accent-warning-default);\n    padding-left: var(--vanna-space-3);\n  }\n\n  .notification-content.level-error {\n    border-left: 4px solid var(--vanna-accent-negative-default);\n    padding-left: var(--vanna-space-3);\n  }\n\n  .notification-icon {\n    font-size: 1.5rem;\n    line-height: 1;\n  }\n\n  .notification-body {\n    flex: 1;\n    padding-right: var(--vanna-space-6);\n  }\n\n  .notification-title {\n    margin-bottom: var(--vanna-space-2);\n    font-size: 0.95rem;\n    font-weight: 600;\n    color: var(--vanna-foreground-default);\n  }\n\n  .notification-message {\n    margin: 0;\n    font-size: 0.875rem;\n    line-height: 1.5;\n    color: var(--vanna-foreground-dimmer);\n  }\n\n  .notification-actions {\n    margin-top: var(--vanna-space-3);\n    display: flex;\n    gap: var(--vanna-space-2);\n    flex-wrap: wrap;\n  }\n\n  .notification-action {\n    padding: var(--vanna-space-2) var(--vanna-space-4);\n    border-radius: var(--vanna-border-radius-md);\n    border: 1px solid var(--vanna-outline-default);\n    background: transparent;\n    color: var(--vanna-foreground-default);\n    cursor: pointer;\n    transition: background var(--vanna-duration-200) ease;\n    font-size: 0.875rem;\n  }\n\n  .notification-action:hover {\n    background: var(--vanna-background-higher);\n  }\n\n  .notification-action.primary {\n    background: var(--vanna-accent-primary-default);\n    border-color: var(--vanna-accent-primary-default);\n    color: white;\n  }\n\n  .notification-action.primary:hover {\n    background: var(--vanna-accent-primary-stronger);\n  }\n\n  .notification-action.secondary {\n    background: var(--vanna-background-default);\n  }\n\n  .notification-dismiss {\n    position: absolute;\n    top: 0;\n    right: 0;\n    background: none;\n    border: none;\n    color: var(--vanna-foreground-dimmer);\n    font-size: 1.2rem;\n    cursor: pointer;\n    padding: var(--vanna-space-1);\n    line-height: 1;\n    transition: color var(--vanna-duration-200) ease;\n  }\n\n  .notification-dismiss:hover {\n    color: var(--vanna-foreground-default);\n  }\n\n  /* Status indicator */\n  .rich-status-indicator {\n    padding: var(--vanna-space-3) var(--vanna-space-4);\n    font-family: var(--vanna-font-family-default);\n  }\n\n  .status-indicator-content {\n    display: inline-flex;\n    align-items: center;\n    gap: var(--vanna-space-2);\n    padding: var(--vanna-space-2) var(--vanna-space-3);\n    border-radius: var(--vanna-border-radius-md);\n    font-size: 0.85rem;\n    font-weight: 500;\n    background: rgba(0, 123, 255, 0.12);\n    color: var(--vanna-accent-primary-default);\n  }\n\n  .status-indicator-content.status-success {\n    background: rgba(16, 185, 129, 0.12);\n    color: var(--vanna-accent-positive-default);\n  }\n\n  .status-indicator-content.status-error {\n    background: rgba(239, 68, 68, 0.12);\n    color: var(--vanna-accent-negative-default);\n  }\n\n  .status-indicator-content.status-warning {\n    background: rgba(245, 158, 11, 0.12);\n    color: var(--vanna-accent-warning-default);\n  }\n\n  .status-indicator-content.status-info {\n    background: rgba(0, 123, 255, 0.12);\n    color: var(--vanna-accent-primary-default);\n  }\n\n  .status-indicator-content.pulse {\n    animation: statusPulse 1.4s ease-in-out infinite;\n  }\n\n  .status-icon {\n    font-size: 1.1rem;\n  }\n\n  @keyframes statusPulse {\n    0%, 100% { box-shadow: 0 0 0 0 rgba(0, 123, 255, 0.35); }\n    50% { box-shadow: 0 0 0 4px rgba(0, 123, 255, 0); }\n  }\n\n  /* Text components */\n  .text-markdown {\n    padding-left: var(--vanna-space-4);\n    line-height: 1.6;\n    font-family: var(--vanna-font-family-default);\n  }\n\n  .text-markdown h1,\n  .text-markdown h2,\n  .text-markdown h3,\n  .text-markdown h4,\n  .text-markdown h5,\n  .text-markdown h6 {\n    margin: var(--vanna-space-3) 0 var(--vanna-space-2) 0;\n    color: var(--vanna-foreground-default);\n  }\n\n  .text-markdown h1:first-child,\n  .text-markdown h2:first-child,\n  .text-markdown h3:first-child,\n  .text-markdown h4:first-child,\n  .text-markdown h5:first-child,\n  .text-markdown h6:first-child {\n    margin-top: 0;\n  }\n\n  .text-markdown p {\n    margin: var(--vanna-space-2) 0;\n    color: var(--vanna-foreground-default);\n  }\n\n  .text-markdown ul,\n  .text-markdown ol {\n    margin: var(--vanna-space-2) 0;\n    padding-left: var(--vanna-space-5);\n  }\n\n  .text-markdown li {\n    margin: var(--vanna-space-1) 0;\n    color: var(--vanna-foreground-default);\n  }\n\n  .text-markdown code {\n    background: var(--vanna-background-root);\n    border: 1px solid var(--vanna-outline-dimmer);\n    border-radius: var(--vanna-border-radius-sm);\n    padding: 2px 4px;\n    font-family: var(--vanna-font-family-mono);\n    font-size: 0.9em;\n    color: var(--vanna-foreground-default);\n  }\n\n  .text-markdown pre {\n    background: var(--vanna-background-root);\n    border: 1px solid var(--vanna-outline-dimmer);\n    border-radius: var(--vanna-border-radius-md);\n    padding: var(--vanna-space-3);\n    overflow-x: auto;\n    margin: var(--vanna-space-3) 0;\n  }\n\n  .text-markdown pre code {\n    background: none;\n    border: none;\n    padding: 0;\n  }\n\n  /* Chart */\n  .rich-chart {\n    padding: var(--vanna-space-4);\n  }\n\n  .chart-header {\n    margin-bottom: var(--vanna-space-3);\n  }\n\n  .chart-title {\n    font-size: 1.125rem;\n    font-weight: 600;\n    color: var(--vanna-foreground-default);\n    margin: 0;\n  }\n\n  .chart-content {\n    min-height: 300px;\n  }\n\n  .chart-error {\n    padding: var(--vanna-space-4);\n    background: var(--vanna-accent-negative-subtle);\n    border-radius: var(--vanna-border-radius-md);\n    color: var(--vanna-accent-negative-default);\n  }\n\n  .chart-error pre {\n    margin-top: var(--vanna-space-2);\n    padding: var(--vanna-space-2);\n    background: var(--vanna-background-lower);\n    border-radius: var(--vanna-border-radius-sm);\n    font-size: 0.75rem;\n    overflow-x: auto;\n  }\n\n  /* DataFrameComponent */\n  .rich-dataframe {\n    overflow: hidden;\n    font-family: var(--vanna-font-family-default);\n  }\n\n  .dataframe-header {\n    padding: var(--vanna-space-4) var(--vanna-space-5);\n    background: var(--vanna-background-higher);\n    border-bottom: 1px solid var(--vanna-outline-default);\n  }\n\n  .dataframe-title {\n    margin: 0 0 var(--vanna-space-2) 0;\n    font-size: 1rem;\n    color: var(--vanna-foreground-default);\n  }\n\n  .dataframe-description {\n    margin: 0 0 var(--vanna-space-3) 0;\n    font-size: 0.875rem;\n    color: var(--vanna-foreground-dimmer);\n  }\n\n  .dataframe-meta {\n    display: flex;\n    gap: var(--vanna-space-4);\n    font-size: 0.75rem;\n    color: var(--vanna-foreground-dimmest);\n  }\n\n  .dataframe-actions {\n    padding: var(--vanna-space-3) var(--vanna-space-5);\n    background: var(--vanna-background-default);\n    border-bottom: 1px solid var(--vanna-outline-dimmer);\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    gap: var(--vanna-space-3);\n  }\n\n  .dataframe-search {\n    flex: 1;\n    max-width: 300px;\n  }\n\n  .search-input {\n    width: 100%;\n    padding: var(--vanna-space-2) var(--vanna-space-3);\n    border: 1px solid var(--vanna-outline-default);\n    border-radius: var(--vanna-border-radius-md);\n    background: var(--vanna-background-default);\n    color: var(--vanna-foreground-default);\n    font-size: 0.875rem;\n    transition: border-color var(--vanna-duration-200) ease;\n  }\n\n  .search-input:focus {\n    outline: none;\n    border-color: var(--vanna-accent-primary-default);\n    box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.2);\n  }\n\n  .export-btn {\n    padding: var(--vanna-space-2) var(--vanna-space-3);\n    border: 1px solid var(--vanna-outline-default);\n    border-radius: var(--vanna-border-radius-md);\n    background: var(--vanna-background-default);\n    color: var(--vanna-foreground-default);\n    cursor: pointer;\n    font-size: 0.875rem;\n    transition: all var(--vanna-duration-200) ease;\n  }\n\n  .export-btn:hover {\n    background: var(--vanna-background-higher);\n    border-color: var(--vanna-accent-primary-default);\n  }\n\n  .dataframe-table-container {\n    max-height: 600px;\n    overflow: auto;\n    border: 1px solid var(--vanna-outline-dimmer);\n    border-radius: var(--vanna-border-radius-md);\n    margin: var(--vanna-space-4) 0;\n  }\n\n  .dataframe-table {\n    width: 100%;\n    border-collapse: collapse;\n    font-size: 0.875rem;\n    font-family: var(--vanna-font-family-default);\n    table-layout: auto;\n  }\n\n  .dataframe-table.bordered {\n    border: 1px solid var(--vanna-outline-dimmer);\n  }\n\n  .dataframe-table.compact th,\n  .dataframe-table.compact td {\n    padding: var(--vanna-space-1) var(--vanna-space-2);\n  }\n\n  .dataframe-table th {\n    background: var(--vanna-background-higher);\n    color: var(--vanna-foreground-default);\n    font-weight: 600;\n    text-align: left;\n    padding: var(--vanna-space-3) var(--vanna-space-4);\n    border-bottom: 2px solid var(--vanna-outline-default);\n    position: sticky;\n    top: 0;\n    z-index: 1;\n  }\n\n  .dataframe-table th.sortable {\n    cursor: pointer;\n    user-select: none;\n    transition: background-color var(--vanna-duration-200) ease;\n  }\n\n  .dataframe-table th.sortable:hover {\n    background: var(--vanna-background-root);\n  }\n\n  .dataframe-table th .sort-indicator {\n    margin-left: var(--vanna-space-2);\n    color: var(--vanna-foreground-dimmer);\n    font-size: 0.8rem;\n  }\n\n  .dataframe-table td {\n    padding: var(--vanna-space-3) var(--vanna-space-4);\n    border-bottom: 1px solid var(--vanna-outline-dimmer);\n    color: var(--vanna-foreground-default);\n  }\n\n  .dataframe-table.striped tbody tr:nth-child(even) {\n    background: rgba(255, 255, 255, 0.02);\n  }\n\n  .dataframe-table tbody tr:hover {\n    background: var(--vanna-background-higher);\n  }\n\n  .dataframe-table .cell-number {\n    text-align: right;\n    font-family: var(--vanna-font-family-mono);\n  }\n\n  .dataframe-table .cell-boolean {\n    text-align: center;\n    font-weight: 600;\n  }\n\n  .dataframe-table .cell-date {\n    font-family: var(--vanna-font-family-mono);\n  }\n\n  .dataframe-table .null-value {\n    color: var(--vanna-foreground-dimmest);\n    font-style: italic;\n  }\n\n  .dataframe-truncated {\n    padding: var(--vanna-space-3) var(--vanna-space-5);\n    text-align: center;\n    color: var(--vanna-foreground-dimmer);\n    background: var(--vanna-background-root);\n    border-top: 1px solid var(--vanna-outline-dimmer);\n    font-size: 0.875rem;\n  }\n\n  .dataframe-empty {\n    padding: var(--vanna-space-8) var(--vanna-space-5);\n    text-align: center;\n    color: var(--vanna-foreground-dimmer);\n  }\n\n  .dataframe-empty p {\n    margin: 0;\n    font-size: 0.875rem;\n  }\n\n  /* Primitive Component Styles */\n\n  /* Status Card */\n  .rich-status-card {\n    overflow: hidden;\n  }\n\n  .status-card-header {\n    display: flex;\n    align-items: center;\n    gap: var(--vanna-space-3);\n    padding: var(--vanna-space-4) var(--vanna-space-5);\n    background: var(--vanna-background-higher);\n    border-bottom: 1px solid var(--vanna-outline-default);\n  }\n\n  .status-card-header.collapsible {\n    cursor: pointer;\n  }\n\n  .status-card-icon {\n    font-size: 1.25rem;\n    display: flex;\n    align-items: center;\n  }\n\n  .status-card-title-section {\n    flex: 1;\n    display: flex;\n    align-items: center;\n    gap: var(--vanna-space-3);\n  }\n\n  .status-card-title {\n    margin: 0;\n    font-size: 1rem;\n    font-weight: 600;\n    color: var(--vanna-foreground-default);\n  }\n\n  .status-card-badge {\n    padding: var(--vanna-space-1) var(--vanna-space-2);\n    border-radius: var(--vanna-border-radius-md);\n    font-size: 0.75rem;\n    font-weight: 600;\n    text-transform: uppercase;\n  }\n\n  .status-card-badge.status-pending {\n    background: var(--vanna-background-root);\n    color: var(--vanna-foreground-dimmer);\n  }\n\n  .status-card-badge.status-running {\n    background: rgba(59, 130, 246, 0.1);\n    color: rgb(37, 99, 235);\n  }\n\n  .status-card-badge.status-success,\n  .status-card-badge.status-completed {\n    background: rgba(16, 185, 129, 0.1);\n    color: rgb(5, 150, 105);\n  }\n\n  .status-card-badge.status-error,\n  .status-card-badge.status-failed {\n    background: rgba(239, 68, 68, 0.1);\n    color: rgb(220, 38, 38);\n  }\n\n  .status-card-badge.status-warning {\n    background: rgba(245, 158, 11, 0.1);\n    color: rgb(217, 119, 6);\n  }\n\n  .status-card-content {\n    padding: var(--vanna-space-4) var(--vanna-space-5);\n    line-height: 1.5;\n    transition: all var(--vanna-duration-200) ease;\n    overflow: hidden;\n  }\n\n  .status-card-content.collapsed {\n    max-height: 0;\n    padding-top: 0;\n    padding-bottom: 0;\n  }\n\n  .status-card-metadata {\n    border-top: 1px solid var(--vanna-outline-default);\n    margin: 0;\n  }\n\n  .status-card-metadata-summary {\n    padding: var(--vanna-space-3) var(--vanna-space-5);\n    cursor: pointer;\n    font-size: 0.875rem;\n    font-weight: 500;\n    color: var(--vanna-foreground-dimmer);\n    user-select: none;\n    transition: background var(--vanna-duration-200) ease;\n  }\n\n  .status-card-metadata-summary:hover {\n    background: var(--vanna-background-higher);\n  }\n\n  .status-card-metadata-content {\n    padding: var(--vanna-space-3) var(--vanna-space-5);\n    background: var(--vanna-background-root);\n  }\n\n  .metadata-table {\n    width: 100%;\n    border-collapse: collapse;\n    font-size: 0.875rem;\n  }\n\n  .metadata-table thead {\n    background: var(--vanna-background-higher);\n  }\n\n  .metadata-table th {\n    text-align: left;\n    padding: var(--vanna-space-2) var(--vanna-space-3);\n    font-weight: 600;\n    color: var(--vanna-foreground-dimmer);\n    border-bottom: 1px solid var(--vanna-outline-default);\n  }\n\n  .metadata-table td {\n    padding: var(--vanna-space-2) var(--vanna-space-3);\n    border-bottom: 1px solid var(--vanna-outline-dimmer);\n    vertical-align: top;\n  }\n\n  .metadata-table tbody tr:last-child td {\n    border-bottom: none;\n  }\n\n  .metadata-key {\n    font-weight: 500;\n    color: var(--vanna-foreground-default);\n    width: 30%;\n  }\n\n  .metadata-value {\n    color: var(--vanna-foreground-default);\n    font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Monaco, Consolas, 'Courier New', monospace;\n  }\n\n  .metadata-string {\n    color: var(--vanna-foreground-default);\n  }\n\n  .metadata-number {\n    color: rgb(37, 99, 235);\n  }\n\n  .metadata-boolean {\n    color: rgb(124, 58, 237);\n  }\n\n  .metadata-null,\n  .metadata-undefined {\n    color: var(--vanna-foreground-dimmer);\n    font-style: italic;\n  }\n\n  .metadata-json {\n    margin: 0;\n    padding: var(--vanna-space-2);\n    background: var(--vanna-background-default);\n    border: 1px solid var(--vanna-outline-default);\n    border-radius: var(--vanna-border-radius-sm);\n    font-size: 0.813rem;\n    line-height: 1.5;\n    overflow-x: auto;\n  }\n\n  /* Progress Display */\n  .rich-progress-display .progress-display-container {\n    padding: var(--vanna-space-4);\n  }\n\n  .progress-display-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    margin-bottom: var(--vanna-space-3);\n  }\n\n  .progress-display-label {\n    font-weight: 500;\n  }\n\n  .progress-display-percentage {\n    font-size: 0.875rem;\n    color: var(--vanna-foreground-dimmer);\n    font-weight: 600;\n  }\n\n  .progress-display-track {\n    height: 12px;\n    background: var(--vanna-background-root);\n    border-radius: 6px;\n    overflow: hidden;\n    border: 1px solid var(--vanna-outline-default);\n  }\n\n  .progress-display-fill {\n    height: 100%;\n    background: var(--vanna-accent-primary-default);\n    border-radius: 6px;\n    transition: width var(--vanna-duration-300) ease;\n    position: relative;\n    overflow: hidden;\n  }\n\n  .progress-display-fill.animated {\n    animation: progressPulse 2s ease-in-out infinite;\n  }\n\n  .progress-display-fill.status-success {\n    background: var(--vanna-accent-positive-default);\n  }\n\n  .progress-display-fill.status-warning {\n    background: var(--vanna-accent-warning-default);\n  }\n\n  .progress-display-fill.status-error {\n    background: var(--vanna-accent-negative-default);\n  }\n\n  .progress-display-description {\n    margin-top: var(--vanna-space-2);\n    font-size: 0.875rem;\n    color: var(--vanna-foreground-dimmer);\n    line-height: 1.4;\n  }\n\n  /* Log Viewer */\n  .rich-log-viewer .log-viewer-container {\n    overflow: hidden;\n  }\n\n  .log-viewer-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    padding: var(--vanna-space-4) var(--vanna-space-5);\n    background: var(--vanna-background-higher);\n    border-bottom: 1px solid var(--vanna-outline-default);\n  }\n\n  .log-viewer-title {\n    margin: 0;\n    font-size: 1rem;\n    font-weight: 600;\n  }\n\n  .log-viewer-search {\n    display: flex;\n    gap: var(--vanna-space-2);\n  }\n\n  .log-search-input {\n    padding: var(--vanna-space-2);\n    border: 1px solid var(--vanna-outline-default);\n    border-radius: var(--vanna-border-radius-md);\n    background: var(--vanna-background-default);\n    color: var(--vanna-foreground-default);\n    font-size: 0.875rem;\n  }\n\n  .log-viewer-content {\n    max-height: 300px;\n    overflow-y: auto;\n    padding: var(--vanna-space-4);\n  }\n\n  .log-viewer-content.auto-scroll {\n    scroll-behavior: smooth;\n  }\n\n  .log-entry {\n    display: flex;\n    gap: var(--vanna-space-2);\n    padding: var(--vanna-space-2) 0;\n    font-family: var(--vanna-font-family-mono);\n    font-size: 0.875rem;\n    line-height: 1.4;\n    border-bottom: 1px solid var(--vanna-outline-default);\n  }\n\n  .log-entry:last-child {\n    border-bottom: none;\n  }\n\n  .log-timestamp {\n    color: var(--vanna-foreground-dimmer);\n    white-space: nowrap;\n  }\n\n  .log-level {\n    font-weight: 600;\n    white-space: nowrap;\n  }\n\n  .log-entry.log-info .log-level {\n    color: var(--vanna-accent-primary-default);\n  }\n\n  .log-entry.log-error .log-level {\n    color: var(--vanna-accent-negative-default);\n  }\n\n  .log-entry.log-warning .log-level {\n    color: var(--vanna-accent-warning-default);\n  }\n\n  .log-entry.log-debug .log-level {\n    color: var(--vanna-foreground-dimmer);\n  }\n\n  .log-message {\n    flex: 1;\n    word-break: break-word;\n  }\n\n  /* Badge */\n  .rich-badge {\n    display: inline-flex;\n    align-items: center;\n    gap: var(--vanna-space-1);\n    padding: var(--vanna-space-1) var(--vanna-space-2);\n    border-radius: var(--vanna-border-radius-full);\n    font-size: 0.75rem;\n    font-weight: 600;\n    text-transform: uppercase;\n    letter-spacing: 0.025em;\n  }\n\n  .rich-badge.badge-small {\n    padding: 2px var(--vanna-space-1);\n    font-size: 0.625rem;\n  }\n\n  .rich-badge.badge-large {\n    padding: var(--vanna-space-2) var(--vanna-space-3);\n    font-size: 0.875rem;\n  }\n\n  .rich-badge.badge-default {\n    background: var(--vanna-background-root);\n    color: var(--vanna-foreground-dimmer);\n  }\n\n  .rich-badge.badge-primary {\n    background: var(--vanna-accent-primary-default);\n    color: white;\n  }\n\n  .rich-badge.badge-success {\n    background: var(--vanna-accent-positive-default);\n    color: white;\n  }\n\n  .rich-badge.badge-warning {\n    background: var(--vanna-accent-warning-default);\n    color: white;\n  }\n\n  .rich-badge.badge-error {\n    background: var(--vanna-accent-negative-default);\n    color: white;\n  }\n\n  /* Icon Text */\n  .rich-icon-text {\n    display: inline-flex;\n    align-items: center;\n    gap: var(--vanna-space-2);\n  }\n\n  .rich-icon-text.icon-text-small {\n    font-size: 0.875rem;\n    gap: var(--vanna-space-1);\n  }\n\n  .rich-icon-text.icon-text-large {\n    font-size: 1.125rem;\n    gap: var(--vanna-space-3);\n  }\n\n  .rich-icon-text.icon-text-center {\n    justify-content: center;\n  }\n\n  .rich-icon-text.icon-text-right {\n    justify-content: flex-end;\n  }\n\n  .icon-text-icon {\n    display: flex;\n    align-items: center;\n  }\n\n  .rich-icon-text.icon-text-primary {\n    color: var(--vanna-accent-primary-default);\n  }\n\n  .rich-icon-text.icon-text-secondary {\n    color: var(--vanna-foreground-dimmer);\n  }\n\n  .rich-icon-text.icon-text-muted {\n    color: var(--vanna-foreground-dimmest);\n  }\n\n  /* Artifact Component Styles */\n  .rich-artifact {\n    overflow: hidden;\n  }\n\n  .artifact-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: flex-start;\n    padding: var(--vanna-space-4) var(--vanna-space-5);\n    background: var(--vanna-background-subtle);\n    border-bottom: 1px solid var(--vanna-outline-default);\n  }\n\n  .artifact-meta {\n    flex: 1;\n  }\n\n  .artifact-title {\n    margin: 0 0 var(--vanna-space-2) 0;\n    font-size: 1.1rem;\n    font-weight: 600;\n    color: var(--vanna-foreground-default);\n  }\n\n  .artifact-description {\n    margin: 0 0 var(--vanna-space-3) 0;\n    color: var(--vanna-foreground-dimmer);\n    font-size: 0.9rem;\n  }\n\n  .artifact-type-badge {\n    display: inline-block;\n    padding: var(--vanna-space-1) var(--vanna-space-2);\n    background: var(--vanna-accent-primary-subtle);\n    color: var(--vanna-accent-primary-default);\n    border-radius: var(--vanna-border-radius-sm);\n    font-size: 0.75rem;\n    font-weight: 500;\n    text-transform: uppercase;\n  }\n\n  .artifact-controls {\n    display: flex;\n    gap: var(--vanna-space-2);\n  }\n\n  .artifact-btn {\n    padding: var(--vanna-space-2);\n    background: var(--vanna-background-default);\n    border: 1px solid var(--vanna-outline-default);\n    border-radius: var(--vanna-border-radius-sm);\n    cursor: pointer;\n    font-size: 1rem;\n    transition: all var(--vanna-duration-200) ease;\n  }\n\n  .artifact-btn:hover {\n    background: var(--vanna-background-subtle);\n    border-color: var(--vanna-outline-hover);\n  }\n\n  .artifact-btn:active {\n    transform: translateY(1px);\n  }\n\n  .artifact-preview {\n    height: 300px;\n    background: var(--vanna-background-default);\n  }\n\n  .artifact-iframe {\n    width: 100%;\n    height: 100%;\n    border: none;\n    display: block;\n  }\n\n  /* Fullscreen overlay styles */\n  .artifact-fullscreen-overlay {\n    position: fixed !important;\n    top: 0 !important;\n    left: 0 !important;\n    width: 100vw !important;\n    height: 100vh !important;\n    background: var(--vanna-background-default) !important;\n    z-index: 10000 !important;\n    display: flex !important;\n    flex-direction: column !important;\n  }\n\n  .fullscreen-header {\n    padding: var(--vanna-space-4) !important;\n    border-bottom: 1px solid var(--vanna-outline-default) !important;\n    display: flex !important;\n    justify-content: space-between !important;\n    align-items: center !important;\n    background: var(--vanna-background-subtle) !important;\n  }\n\n  .fullscreen-header h3 {\n    margin: 0 !important;\n    color: var(--vanna-foreground-default) !important;\n  }\n\n  .close-fullscreen {\n    padding: var(--vanna-space-2) var(--vanna-space-3) !important;\n    background: var(--vanna-background-default) !important;\n    border: 1px solid var(--vanna-outline-default) !important;\n    border-radius: var(--vanna-border-radius-sm) !important;\n    cursor: pointer !important;\n    font-size: 1.2rem !important;\n    line-height: 1 !important;\n  }\n\n  .close-fullscreen:hover {\n    background: var(--vanna-background-subtle) !important;\n  }\n\n  .fullscreen-content {\n    flex: 1 !important;\n    padding: var(--vanna-space-4) !important;\n    overflow: hidden !important;\n  }\n\n  .fullscreen-iframe {\n    width: 100% !important;\n    height: 100% !important;\n    border: none !important;\n    border-radius: var(--vanna-border-radius-md) !important;\n  }\n\n  /* Artifact placeholder styles */\n  .artifact-placeholder {\n    padding: var(--vanna-space-4);\n    background: var(--vanna-background-subtle);\n    border: 2px dashed var(--vanna-outline-default);\n    border-radius: var(--vanna-border-radius-md);\n    text-align: center;\n  }\n\n  .placeholder-content {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    gap: var(--vanna-space-3);\n    opacity: 0.8;\n  }\n\n  .placeholder-icon {\n    font-size: 1.5rem;\n  }\n\n  .placeholder-text {\n    text-align: left;\n  }\n\n  .placeholder-text strong {\n    color: var(--vanna-foreground-default);\n    font-weight: 600;\n  }\n\n  .placeholder-type {\n    font-size: 0.8rem;\n    color: var(--vanna-foreground-dimmer);\n    text-transform: uppercase;\n    margin-top: var(--vanna-space-1);\n  }\n\n  .placeholder-reopen {\n    padding: var(--vanna-space-2);\n    background: var(--vanna-accent-primary-default);\n    color: white;\n    border: none;\n    border-radius: var(--vanna-border-radius-sm);\n    cursor: pointer;\n    font-size: 1rem;\n    transition: background var(--vanna-duration-200) ease;\n  }\n\n  .placeholder-reopen:hover {\n    background: var(--vanna-accent-primary-hover);\n  }\n\n  /* Button Component */\n  .rich-button {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    gap: var(--vanna-space-2);\n    padding: var(--vanna-space-2) var(--vanna-space-4);\n    border-radius: var(--vanna-border-radius-md);\n    border: 1px solid;\n    font-size: 0.875rem;\n    font-weight: 500;\n    cursor: pointer;\n    transition: all var(--vanna-duration-200) ease;\n    white-space: nowrap;\n    user-select: none;\n    font-family: var(--vanna-font-family-default);\n  }\n\n  .rich-button:disabled {\n    cursor: not-allowed;\n    opacity: 0.5;\n  }\n\n  /* Button variants */\n  .rich-button.button-primary {\n    background: var(--vanna-accent-primary-default);\n    border-color: var(--vanna-accent-primary-default);\n    color: white;\n  }\n\n  .rich-button.button-primary:hover:not(:disabled) {\n    background: var(--vanna-accent-primary-stronger);\n    border-color: var(--vanna-accent-primary-stronger);\n  }\n\n  .rich-button.button-secondary {\n    background: var(--vanna-background-default);\n    border-color: var(--vanna-outline-default);\n    color: var(--vanna-foreground-default);\n  }\n\n  .rich-button.button-secondary:hover:not(:disabled) {\n    background: var(--vanna-background-higher);\n    border-color: var(--vanna-outline-hover);\n  }\n\n  .rich-button.button-success {\n    background: var(--vanna-accent-positive-default);\n    border-color: var(--vanna-accent-positive-default);\n    color: white;\n  }\n\n  .rich-button.button-success:hover:not(:disabled) {\n    background: var(--vanna-accent-positive-stronger);\n  }\n\n  .rich-button.button-warning {\n    background: var(--vanna-accent-warning-default);\n    border-color: var(--vanna-accent-warning-default);\n    color: white;\n  }\n\n  .rich-button.button-warning:hover:not(:disabled) {\n    background: var(--vanna-accent-warning-stronger);\n  }\n\n  .rich-button.button-error {\n    background: var(--vanna-accent-negative-default);\n    border-color: var(--vanna-accent-negative-default);\n    color: white;\n  }\n\n  .rich-button.button-error:hover:not(:disabled) {\n    background: var(--vanna-accent-negative-stronger);\n  }\n\n  .rich-button.button-ghost {\n    background: transparent;\n    border-color: transparent;\n    color: var(--vanna-foreground-default);\n  }\n\n  .rich-button.button-ghost:hover:not(:disabled) {\n    background: var(--vanna-background-higher);\n  }\n\n  .rich-button.button-link {\n    background: transparent;\n    border-color: transparent;\n    color: var(--vanna-accent-primary-default);\n    text-decoration: underline;\n    padding: var(--vanna-space-1) var(--vanna-space-2);\n  }\n\n  .rich-button.button-link:hover:not(:disabled) {\n    color: var(--vanna-accent-primary-stronger);\n  }\n\n  /* Button sizes */\n  .rich-button.button-small {\n    padding: var(--vanna-space-1) var(--vanna-space-3);\n    font-size: 0.75rem;\n    gap: var(--vanna-space-1);\n  }\n\n  .rich-button.button-medium {\n    padding: var(--vanna-space-2) var(--vanna-space-4);\n    font-size: 0.875rem;\n    gap: var(--vanna-space-2);\n  }\n\n  .rich-button.button-large {\n    padding: var(--vanna-space-3) var(--vanna-space-5);\n    font-size: 1rem;\n    gap: var(--vanna-space-2);\n  }\n\n  /* Button modifiers */\n  .rich-button.button-full-width {\n    width: 100%;\n  }\n\n  .rich-button.button-loading {\n    position: relative;\n    pointer-events: none;\n  }\n\n  .button-spinner {\n    display: inline-flex;\n    animation: spin 1s linear infinite;\n  }\n\n  @keyframes spin {\n    from { transform: rotate(0deg); }\n    to { transform: rotate(360deg); }\n  }\n\n  .button-icon {\n    display: inline-flex;\n    align-items: center;\n    font-size: 1em;\n  }\n\n  .button-label {\n    display: inline-flex;\n    align-items: center;\n  }\n\n  /* Button Group Component */\n  .rich-button-group {\n    display: flex;\n    gap: var(--vanna-space-2);\n    font-family: var(--vanna-font-family-default);\n  }\n\n  .rich-button-group.button-group-vertical {\n    flex-direction: column;\n  }\n\n  .rich-button-group.button-group-horizontal {\n    flex-direction: row;\n  }\n\n  .rich-button-group.button-group-spacing-small {\n    gap: var(--vanna-space-1);\n  }\n\n  .rich-button-group.button-group-spacing-medium {\n    gap: var(--vanna-space-2);\n  }\n\n  .rich-button-group.button-group-spacing-large {\n    gap: var(--vanna-space-4);\n  }\n\n  .rich-button-group.button-group-align-left {\n    justify-content: flex-start;\n  }\n\n  .rich-button-group.button-group-align-center {\n    justify-content: center;\n  }\n\n  .rich-button-group.button-group-align-right {\n    justify-content: flex-end;\n  }\n\n  .rich-button-group.button-group-align-space-between {\n    justify-content: space-between;\n  }\n\n  .rich-button-group.button-group-full-width {\n    width: 100%;\n  }\n\n  .rich-button-group.button-group-full-width > .rich-button {\n    flex: 1;\n  }\n\n  /* Button Group Interactive States */\n  .rich-button.button-transitioning {\n    transition: all 0.2s ease-in-out;\n  }\n\n  .rich-button.button-highlighted {\n    transform: scale(1.02);\n    box-shadow: 0 0 0 2px var(--vanna-accent-primary-default);\n    z-index: 1;\n    position: relative;\n  }\n\n  .rich-button.button-grayed-out {\n    opacity: 0.4;\n    filter: grayscale(50%);\n    transform: scale(0.98);\n  }\n\n  .rich-button.button-clicked {\n    animation: buttonClickPulse 0.3s ease-out;\n  }\n\n  @keyframes buttonClickPulse {\n    0% {\n      transform: scale(1);\n    }\n    50% {\n      transform: scale(1.05);\n    }\n    100% {\n      transform: scale(1.02);\n    }\n  }\n\n  /* Override hover states when in click states */\n  .rich-button.button-highlighted:hover,\n  .rich-button.button-grayed-out:hover {\n    /* Maintain the click state even on hover */\n  }\n\n  .rich-button.button-grayed-out:hover {\n    opacity: 0.4;\n    filter: grayscale(50%);\n  }\n`;\n\nexport const richComponentStyleText = richComponentStyles.cssText;\n"
  },
  {
    "path": "frontends/webcomponent/src/styles/vanna-design-tokens.ts",
    "content": "import { css } from 'lit';\n\n// Vanna 2.0 design tokens - Data-First Agents branding\nexport const vannaDesignTokens = css`\n  :host {\n    /* Vanna 2.0 Brand Colors */\n    --vanna-navy: rgb(2, 61, 96);\n    --vanna-cream: rgb(231, 225, 207);\n    --vanna-teal: rgb(21, 168, 168);\n    --vanna-orange: rgb(254, 93, 38);\n    --vanna-magenta: rgb(191, 19, 99);\n\n    /* Color Palette - Light mode (default) */\n    --vanna-background-root: rgb(255, 255, 255);\n    --vanna-background-default: rgb(231, 225, 207);\n    --vanna-background-higher: rgb(244, 246, 248);\n    --vanna-background-highest: rgb(229, 231, 235);\n    --vanna-background-subtle: rgb(248, 250, 252);\n    --vanna-background-lower: rgb(239, 242, 245);\n\n    --vanna-foreground-default: rgb(2, 61, 96);\n    --vanna-foreground-dimmer: rgb(71, 85, 105);\n    --vanna-foreground-dimmest: rgb(100, 116, 139);\n\n    --vanna-accent-primary-default: rgb(21, 168, 168);\n    --vanna-accent-primary-stronger: rgb(2, 61, 96);\n    --vanna-accent-primary-strongest: rgb(2, 61, 96);\n    --vanna-accent-primary-subtle: rgba(21, 168, 168, 0.1);\n    --vanna-accent-primary-hover: rgb(21, 168, 168);\n\n    --vanna-accent-positive-default: rgb(21, 168, 168);\n    --vanna-accent-positive-stronger: rgb(2, 61, 96);\n    --vanna-accent-positive-subtle: rgba(21, 168, 168, 0.1);\n\n    --vanna-accent-negative-default: rgb(239, 68, 68);\n    --vanna-accent-negative-stronger: rgb(220, 38, 38);\n    --vanna-accent-negative-subtle: rgba(239, 68, 68, 0.1);\n\n    --vanna-accent-warning-default: rgb(254, 93, 38);\n    --vanna-accent-warning-stronger: rgb(254, 93, 38);\n    --vanna-accent-warning-subtle: rgba(254, 93, 38, 0.1);\n\n    /* Outline/Border colors */\n    --vanna-outline-default: rgba(21, 168, 168, 0.3);\n    --vanna-outline-dimmer: rgb(241, 245, 249);\n    --vanna-outline-dimmest: rgb(248, 250, 252);\n    --vanna-outline-hover: rgb(21, 168, 168);\n\n    /* Typography */\n    --vanna-font-family-default: \"Space Grotesk\", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif;\n    --vanna-font-family-serif: \"Roboto Slab\", ui-serif, Georgia, serif;\n    --vanna-font-family-mono: \"Space Mono\", ui-monospace, SFMono-Regular, \"SF Mono\", Monaco, Inconsolata, \"Roboto Mono\", \"Ubuntu Mono\", monospace;\n\n    /* Spacing scale */\n    --vanna-space-0: 0px;\n    --vanna-space-1: 4px;\n    --vanna-space-2: 8px;\n    --vanna-space-3: 12px;\n    --vanna-space-4: 16px;\n    --vanna-space-5: 20px;\n    --vanna-space-6: 24px;\n    --vanna-space-7: 28px;\n    --vanna-space-8: 32px;\n    --vanna-space-10: 40px;\n    --vanna-space-12: 48px;\n    --vanna-space-16: 64px;\n\n    /* Border radius */\n    --vanna-border-radius-sm: 6px;\n    --vanna-border-radius-md: 10px;\n    --vanna-border-radius-lg: 14px;\n    --vanna-border-radius-xl: 20px;\n    --vanna-border-radius-2xl: 24px;\n    --vanna-border-radius-full: 9999px;\n\n    /* Shadows - Preline-inspired */\n    --vanna-shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.05);\n    --vanna-shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);\n    --vanna-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);\n    --vanna-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);\n    --vanna-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);\n    --vanna-shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);\n\n    /* Animation durations */\n    --vanna-duration-75: 75ms;\n    --vanna-duration-100: 100ms;\n    --vanna-duration-150: 150ms;\n    --vanna-duration-200: 200ms;\n    --vanna-duration-300: 300ms;\n    --vanna-duration-500: 500ms;\n    --vanna-duration-700: 700ms;\n\n    /* Z-index scale */\n    --vanna-z-dropdown: 1000;\n    --vanna-z-sticky: 1020;\n    --vanna-z-fixed: 1030;\n    --vanna-z-modal: 1040;\n    --vanna-z-popover: 1050;\n    --vanna-z-tooltip: 1060;\n\n    /* Chat-specific tokens */\n    --vanna-chat-bubble-radius: 18px;\n    --vanna-chat-bubble-radius-sm: 12px;\n    --vanna-chat-spacing: 16px;\n    --vanna-chat-avatar-size: 40px;\n  }\n\n  /* Dark theme overrides */\n  :host([theme=\"dark\"]) {\n    --vanna-background-root: rgb(9, 11, 17);\n    --vanna-background-default: rgb(15, 18, 25);\n    --vanna-background-higher: rgb(24, 29, 39);\n    --vanna-background-highest: rgb(31, 39, 51);\n    --vanna-background-subtle: rgb(17, 21, 28);\n    --vanna-background-lower: rgb(6, 8, 12);\n\n    --vanna-foreground-default: rgb(248, 250, 252);\n    --vanna-foreground-dimmer: rgb(203, 213, 225);\n    --vanna-foreground-dimmest: rgb(148, 163, 184);\n\n    --vanna-accent-primary-default: rgb(21, 168, 168);\n    --vanna-accent-primary-stronger: rgb(21, 168, 168);\n    --vanna-accent-primary-strongest: rgb(2, 61, 96);\n    --vanna-accent-primary-subtle: rgba(21, 168, 168, 0.15);\n    --vanna-accent-primary-hover: rgb(21, 168, 168);\n\n    --vanna-accent-positive-default: rgb(21, 168, 168);\n    --vanna-accent-positive-stronger: rgb(21, 168, 168);\n    --vanna-accent-positive-subtle: rgba(21, 168, 168, 0.15);\n\n    --vanna-accent-negative-default: rgb(248, 113, 113);\n    --vanna-accent-negative-stronger: rgb(239, 68, 68);\n    --vanna-accent-negative-subtle: rgba(248, 113, 113, 0.15);\n\n    --vanna-accent-warning-default: rgb(254, 93, 38);\n    --vanna-accent-warning-stronger: rgb(254, 93, 38);\n    --vanna-accent-warning-subtle: rgba(254, 93, 38, 0.15);\n\n    --vanna-outline-default: rgba(21, 168, 168, 0.3);\n    --vanna-outline-dimmer: rgb(31, 41, 55);\n    --vanna-outline-dimmest: rgb(17, 24, 39);\n    --vanna-outline-hover: rgb(21, 168, 168);\n\n    --vanna-shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.6);\n    --vanna-shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.5), 0 1px 2px -1px rgba(0, 0, 0, 0.5);\n    --vanna-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.4);\n    --vanna-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -4px rgba(0, 0, 0, 0.4);\n    --vanna-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.3);\n    --vanna-shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.6);\n  }\n`;\n"
  },
  {
    "path": "frontends/webcomponent/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n\ndeclare const __BUILD_TIME__: string;\ndeclare const __BUILD_VERSION__: string;\n"
  },
  {
    "path": "frontends/webcomponent/test-comprehensive.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Vanna Webcomponent - Comprehensive Test</title>\n    <style>\n        * {\n            margin: 0;\n            padding: 0;\n            box-sizing: border-box;\n        }\n\n        body {\n            font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n            background: #f5f5f5;\n            display: flex;\n            height: 100vh;\n            overflow: hidden;\n        }\n\n        .sidebar {\n            width: 300px;\n            background: white;\n            border-right: 1px solid #e0e0e0;\n            display: flex;\n            flex-direction: column;\n            overflow-y: auto;\n        }\n\n        .sidebar-header {\n            padding: 20px;\n            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n            color: white;\n        }\n\n        .sidebar-header h1 {\n            font-size: 18px;\n            margin-bottom: 8px;\n        }\n\n        .sidebar-header p {\n            font-size: 12px;\n            opacity: 0.9;\n        }\n\n        .controls {\n            padding: 20px;\n            border-bottom: 1px solid #e0e0e0;\n        }\n\n        .control-group {\n            margin-bottom: 15px;\n        }\n\n        .control-group label {\n            display: block;\n            font-size: 12px;\n            font-weight: 600;\n            color: #666;\n            margin-bottom: 5px;\n            text-transform: uppercase;\n            letter-spacing: 0.5px;\n        }\n\n        button {\n            width: 100%;\n            padding: 10px 15px;\n            background: #667eea;\n            color: white;\n            border: none;\n            border-radius: 6px;\n            font-size: 14px;\n            font-weight: 600;\n            cursor: pointer;\n            transition: all 0.2s;\n        }\n\n        button:hover {\n            background: #5568d3;\n            transform: translateY(-1px);\n            box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3);\n        }\n\n        button:active {\n            transform: translateY(0);\n        }\n\n        button.secondary {\n            background: #f0f0f0;\n            color: #333;\n        }\n\n        button.secondary:hover {\n            background: #e0e0e0;\n            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);\n        }\n\n        select {\n            width: 100%;\n            padding: 8px 12px;\n            border: 1px solid #e0e0e0;\n            border-radius: 6px;\n            font-size: 14px;\n            background: white;\n            cursor: pointer;\n        }\n\n        .status-section {\n            padding: 20px;\n        }\n\n        .status-indicator {\n            display: flex;\n            align-items: center;\n            gap: 10px;\n            padding: 12px;\n            background: #f8f9fa;\n            border-radius: 6px;\n            margin-bottom: 10px;\n        }\n\n        .status-dot {\n            width: 12px;\n            height: 12px;\n            border-radius: 50%;\n            background: #ccc;\n        }\n\n        .status-dot.success {\n            background: #10b981;\n            animation: pulse 2s infinite;\n        }\n\n        .status-dot.error {\n            background: #ef4444;\n            animation: pulse 2s infinite;\n        }\n\n        .status-dot.warning {\n            background: #f59e0b;\n        }\n\n        @keyframes pulse {\n            0%, 100% { opacity: 1; }\n            50% { opacity: 0.5; }\n        }\n\n        .status-text {\n            font-size: 13px;\n            font-weight: 500;\n            color: #333;\n        }\n\n        .console-monitor {\n            padding: 20px;\n            border-top: 1px solid #e0e0e0;\n        }\n\n        .console-monitor h3 {\n            font-size: 14px;\n            font-weight: 600;\n            margin-bottom: 10px;\n            color: #333;\n        }\n\n        .console-log {\n            background: #1e1e1e;\n            color: #d4d4d4;\n            padding: 12px;\n            border-radius: 6px;\n            font-family: 'Courier New', monospace;\n            font-size: 11px;\n            max-height: 200px;\n            overflow-y: auto;\n        }\n\n        .console-log .error {\n            color: #f48771;\n        }\n\n        .console-log .warning {\n            color: #dcdcaa;\n        }\n\n        .console-log .info {\n            color: #4fc1ff;\n        }\n\n        .checklist {\n            padding: 20px;\n            border-top: 1px solid #e0e0e0;\n        }\n\n        .checklist h3 {\n            font-size: 14px;\n            font-weight: 600;\n            margin-bottom: 10px;\n            color: #333;\n        }\n\n        .checklist-item {\n            display: flex;\n            align-items: center;\n            gap: 8px;\n            padding: 6px 0;\n            font-size: 12px;\n            color: #666;\n        }\n\n        .checklist-item .check {\n            width: 16px;\n            height: 16px;\n            border: 2px solid #e0e0e0;\n            border-radius: 3px;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            font-size: 10px;\n        }\n\n        .checklist-item.checked .check {\n            background: #10b981;\n            border-color: #10b981;\n            color: white;\n        }\n\n        .main-content {\n            flex: 1;\n            display: flex;\n            flex-direction: column;\n            overflow: hidden;\n        }\n\n        .chat-container {\n            flex: 1;\n            overflow: hidden;\n        }\n\n        vanna-chat {\n            width: 100%;\n            height: 100%;\n        }\n\n        .metrics {\n            padding: 10px 20px;\n            background: white;\n            border-top: 1px solid #e0e0e0;\n            display: flex;\n            gap: 20px;\n            font-size: 12px;\n            color: #666;\n        }\n\n        .metric {\n            display: flex;\n            gap: 5px;\n        }\n\n        .metric strong {\n            color: #333;\n        }\n    </style>\n</head>\n<body>\n    <!-- Sidebar -->\n    <div class=\"sidebar\">\n        <div class=\"sidebar-header\">\n            <h1>Component Test Suite</h1>\n            <p>Comprehensive validation for webcomponent pruning</p>\n        </div>\n\n        <!-- Controls -->\n        <div class=\"controls\">\n            <div class=\"control-group\">\n                <label>Test Mode</label>\n                <select id=\"mode-select\">\n                    <option value=\"realistic\">Realistic (with delays)</option>\n                    <option value=\"rapid\">Rapid (fast)</option>\n                </select>\n            </div>\n\n            <div class=\"control-group\">\n                <button id=\"start-test\">Run Comprehensive Test</button>\n            </div>\n\n            <div class=\"control-group\">\n                <button class=\"secondary\" id=\"clear-chat\">Clear Chat</button>\n            </div>\n        </div>\n\n        <!-- Status -->\n        <div class=\"status-section\">\n            <div class=\"status-indicator\">\n                <div class=\"status-dot\" id=\"backend-status\"></div>\n                <div class=\"status-text\" id=\"backend-text\">Checking backend...</div>\n            </div>\n\n            <div class=\"status-indicator\">\n                <div class=\"status-dot\" id=\"console-status\"></div>\n                <div class=\"status-text\" id=\"console-text\">No errors detected</div>\n            </div>\n        </div>\n\n        <!-- Component Checklist -->\n        <div class=\"checklist\">\n            <h3>Component Rendering</h3>\n            <div class=\"checklist-item\" data-component=\"text\">\n                <div class=\"check\"></div>\n                <span>Text Component</span>\n            </div>\n            <div class=\"checklist-item\" data-component=\"status_card\">\n                <div class=\"check\"></div>\n                <span>Status Card</span>\n            </div>\n            <div class=\"checklist-item\" data-component=\"progress_display\">\n                <div class=\"check\"></div>\n                <span>Progress Display</span>\n            </div>\n            <div class=\"checklist-item\" data-component=\"card\">\n                <div class=\"check\"></div>\n                <span>Card</span>\n            </div>\n            <div class=\"checklist-item\" data-component=\"task_list\">\n                <div class=\"check\"></div>\n                <span>Task List</span>\n            </div>\n            <div class=\"checklist-item\" data-component=\"progress_bar\">\n                <div class=\"check\"></div>\n                <span>Progress Bar</span>\n            </div>\n            <div class=\"checklist-item\" data-component=\"notification\">\n                <div class=\"check\"></div>\n                <span>Notification</span>\n            </div>\n            <div class=\"checklist-item\" data-component=\"status_indicator\">\n                <div class=\"check\"></div>\n                <span>Status Indicator</span>\n            </div>\n            <div class=\"checklist-item\" data-component=\"badge\">\n                <div class=\"check\"></div>\n                <span>Badge</span>\n            </div>\n            <div class=\"checklist-item\" data-component=\"icon_text\">\n                <div class=\"check\"></div>\n                <span>Icon Text</span>\n            </div>\n            <div class=\"checklist-item\" data-component=\"button\">\n                <div class=\"check\"></div>\n                <span>Button</span>\n            </div>\n            <div class=\"checklist-item\" data-component=\"button_group\">\n                <div class=\"check\"></div>\n                <span>Button Group</span>\n            </div>\n            <div class=\"checklist-item\" data-component=\"dataframe\">\n                <div class=\"check\"></div>\n                <span>DataFrame</span>\n            </div>\n            <div class=\"checklist-item\" data-component=\"chart\">\n                <div class=\"check\"></div>\n                <span>Chart</span>\n            </div>\n            <div class=\"checklist-item\" data-component=\"artifact\">\n                <div class=\"check\"></div>\n                <span>Artifact</span>\n            </div>\n            <div class=\"checklist-item\" data-component=\"log_viewer\">\n                <div class=\"check\"></div>\n                <span>Log Viewer</span>\n            </div>\n            <!-- Note: code_block, table, container not supported by webcomponent -->\n        </div>\n\n        <!-- Console Monitor -->\n        <div class=\"console-monitor\">\n            <h3>Console Log</h3>\n            <div class=\"console-log\" id=\"console-log\">\n                <div class=\"info\">Monitoring console for errors...</div>\n            </div>\n        </div>\n    </div>\n\n    <!-- Main Content -->\n    <div class=\"main-content\">\n        <div class=\"chat-container\">\n            <vanna-chat\n                id=\"vanna-chat\"\n                api-url=\"http://localhost:5555\"\n                placeholder=\"Type /test to run comprehensive test...\"\n            ></vanna-chat>\n        </div>\n\n        <div class=\"metrics\">\n            <div class=\"metric\">\n                <strong>Components Rendered:</strong>\n                <span id=\"component-count\">0</span>\n            </div>\n            <div class=\"metric\">\n                <strong>Updates Processed:</strong>\n                <span id=\"update-count\">0</span>\n            </div>\n            <div class=\"metric\">\n                <strong>Errors:</strong>\n                <span id=\"error-count\">0</span>\n            </div>\n        </div>\n    </div>\n\n    <!-- Load webcomponent -->\n    <script type=\"module\" src=\"/static/vanna-components.js\"></script>\n\n    <script type=\"module\">\n        // State\n        let componentCount = 0;\n        let updateCount = 0;\n        let errorCount = 0;\n        const seenComponents = new Set();\n\n        // Elements\n        const vannaChat = document.getElementById('vanna-chat');\n        const modeSelect = document.getElementById('mode-select');\n        const startTestBtn = document.getElementById('start-test');\n        const clearChatBtn = document.getElementById('clear-chat');\n        const consoleLog = document.getElementById('console-log');\n        const backendStatus = document.getElementById('backend-status');\n        const backendText = document.getElementById('backend-text');\n        const consoleStatus = document.getElementById('console-status');\n        const consoleText = document.getElementById('console-text');\n\n        // Check backend health\n        async function checkBackend() {\n            try {\n                const response = await fetch('http://localhost:5555/health');\n                const data = await response.json();\n                backendStatus.className = 'status-dot success';\n                backendText.textContent = `Backend ready (${data.mode} mode)`;\n            } catch (error) {\n                backendStatus.className = 'status-dot error';\n                backendText.textContent = 'Backend not responding';\n                addConsoleLog('error', `Backend health check failed: ${error.message}`);\n            }\n        }\n\n        // Console monitoring\n        function addConsoleLog(type, message) {\n            const timestamp = new Date().toLocaleTimeString();\n            const logEntry = document.createElement('div');\n            logEntry.className = type;\n            logEntry.textContent = `[${timestamp}] ${message}`;\n            consoleLog.appendChild(logEntry);\n            consoleLog.scrollTop = consoleLog.scrollHeight;\n\n            // Update error status\n            if (type === 'error') {\n                errorCount++;\n                consoleStatus.className = 'status-dot error';\n                consoleText.textContent = `${errorCount} error(s) detected`;\n                document.getElementById('error-count').textContent = errorCount;\n            }\n        }\n\n        // Override console methods\n        const originalError = console.error;\n        const originalWarn = console.warn;\n        const originalLog = console.log;\n\n        console.error = function(...args) {\n            addConsoleLog('error', args.join(' '));\n            originalError.apply(console, args);\n        };\n\n        console.warn = function(...args) {\n            addConsoleLog('warning', args.join(' '));\n            originalWarn.apply(console, args);\n        };\n\n        console.log = function(...args) {\n            const message = args.join(' ');\n            if (message.includes('ERROR') || message.includes('Error')) {\n                addConsoleLog('error', message);\n            } else {\n                addConsoleLog('info', message);\n            }\n            originalLog.apply(console, args);\n        };\n\n        // Monitor window errors\n        window.addEventListener('error', (event) => {\n            addConsoleLog('error', `${event.message} at ${event.filename}:${event.lineno}`);\n            errorCount++;\n        });\n\n        // Monitor component rendering\n        const observer = new MutationObserver((mutations) => {\n            mutations.forEach((mutation) => {\n                mutation.addedNodes.forEach((node) => {\n                    if (node.nodeType === 1) { // Element node\n                        const componentType = node.getAttribute('data-component-type');\n                        if (componentType) {\n                            // Track component\n                            if (!seenComponents.has(componentType)) {\n                                seenComponents.add(componentType);\n                                componentCount++;\n                                document.getElementById('component-count').textContent = componentCount;\n\n                                // Check off in checklist\n                                const checklistItem = document.querySelector(`[data-component=\"${componentType}\"]`);\n                                if (checklistItem) {\n                                    checklistItem.classList.add('checked');\n                                    checklistItem.querySelector('.check').textContent = '✓';\n                                }\n                            }\n\n                            updateCount++;\n                            document.getElementById('update-count').textContent = updateCount;\n                        }\n                    }\n                });\n            });\n        });\n\n        // Start observing when vanna-chat is ready\n        setTimeout(() => {\n            const shadowRoot = vannaChat.shadowRoot;\n            if (shadowRoot) {\n                const container = shadowRoot.querySelector('.rich-components-container');\n                if (container) {\n                    observer.observe(container, { childList: true, subtree: true });\n                    addConsoleLog('info', 'Component observer started');\n                }\n            }\n        }, 1000);\n\n        // Event listeners\n        startTestBtn.addEventListener('click', async () => {\n            const mode = modeSelect.value;\n\n            // Update backend mode\n            try {\n                await fetch(`http://localhost:5555/health`);\n                addConsoleLog('info', `Starting comprehensive test in ${mode} mode...`);\n\n                // Send test message through chat\n                vannaChat.dispatchEvent(new CustomEvent('send-message', {\n                    detail: { message: '/test' }\n                }));\n\n                // Alternative: directly trigger if API is exposed\n                const inputEl = vannaChat.shadowRoot?.querySelector('textarea, input');\n                if (inputEl) {\n                    inputEl.value = '/test';\n                    const form = vannaChat.shadowRoot?.querySelector('form');\n                    if (form) {\n                        form.dispatchEvent(new Event('submit', { bubbles: true }));\n                    }\n                }\n            } catch (error) {\n                addConsoleLog('error', `Failed to start test: ${error.message}`);\n            }\n        });\n\n        clearChatBtn.addEventListener('click', () => {\n            // Reset state\n            componentCount = 0;\n            updateCount = 0;\n            errorCount = 0;\n            seenComponents.clear();\n\n            document.getElementById('component-count').textContent = '0';\n            document.getElementById('update-count').textContent = '0';\n            document.getElementById('error-count').textContent = '0';\n\n            // Uncheck all checklist items\n            document.querySelectorAll('.checklist-item').forEach(item => {\n                item.classList.remove('checked');\n                item.querySelector('.check').textContent = '';\n            });\n\n            // Clear console\n            consoleLog.innerHTML = '<div class=\"info\">Console cleared</div>';\n            consoleStatus.className = 'status-dot';\n            consoleText.textContent = 'No errors detected';\n\n            // Reload page to truly clear (vanna-chat doesn't expose clear method)\n            location.reload();\n        });\n\n        // Initial backend check\n        checkBackend();\n        setInterval(checkBackend, 5000);\n\n        // Log startup\n        addConsoleLog('info', 'Test suite initialized');\n        addConsoleLog('info', 'Ensure backend is running: python test_backend.py');\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "frontends/webcomponent/test_backend.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nComprehensive test backend for vanna-webcomponent validation.\n\nThis backend exercises all component types and update patterns to validate\nthat nothing breaks during webcomponent pruning.\n\nUsage:\n    python test_backend.py --mode rapid      # Fast stress test\n    python test_backend.py --mode realistic  # Realistic conversation flow\n\"\"\"\n\nimport argparse\nimport asyncio\nimport json\nimport sys\nimport time\nimport traceback\nimport uuid\nfrom datetime import datetime\nfrom typing import AsyncGenerator, Dict, Any, Optional\n\nfrom fastapi import FastAPI\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom fastapi.responses import StreamingResponse, FileResponse\nfrom fastapi.staticfiles import StaticFiles\nfrom pydantic import BaseModel\nimport os\n\n# Add vanna to path\nsys.path.insert(0, \"../vanna/src\")\n\nfrom vanna.core.rich_component import RichComponent, ComponentLifecycle\nfrom vanna.components.rich import (\n    RichTextComponent,\n    StatusCardComponent,\n    ProgressDisplayComponent,\n    ProgressBarComponent,\n    NotificationComponent,\n    StatusIndicatorComponent,\n    ButtonComponent,\n    ButtonGroupComponent,\n    CardComponent,\n    TaskListComponent,\n    Task,\n    BadgeComponent,\n    IconTextComponent,\n    DataFrameComponent,\n    ChartComponent,\n    ArtifactComponent,\n    LogViewerComponent,\n    LogEntry,\n    StatusBarUpdateComponent,\n    TaskTrackerUpdateComponent,\n    ChatInputUpdateComponent,\n    TaskOperation,\n)\nfrom vanna.servers.base.models import ChatStreamChunk\n\n# Request/Response models\nclass ChatRequest(BaseModel):\n    \"\"\"Chat request matching vanna API.\"\"\"\n    message: str\n    conversation_id: Optional[str] = None\n    request_id: Optional[str] = None\n    request_context: Dict[str, Any] = {}\n\n\nclass UiComponent(BaseModel):\n    \"\"\"UI component wrapper.\"\"\"\n    rich_component: RichComponent\n\n\n# Test state\ntest_state: Dict[str, Any] = {\n    \"mode\": \"realistic\",\n    \"component_ids\": {},  # Track component IDs for updates\n    \"action_count\": 0,\n}\n\n\nasync def yield_chunk(component: RichComponent, conversation_id: str, request_id: str) -> ChatStreamChunk:\n    \"\"\"Convert component to ChatStreamChunk.\"\"\"\n    return ChatStreamChunk(\n        rich=component.serialize_for_frontend(),\n        simple=None,\n        conversation_id=conversation_id,\n        request_id=request_id,\n        timestamp=time.time(),\n    )\n\n\nasync def delay(mode: str, short: float = 0.1, long: float = 0.5):\n    \"\"\"Add delay based on mode.\"\"\"\n    if mode == \"realistic\":\n        await asyncio.sleep(long)\n    elif mode == \"rapid\":\n        await asyncio.sleep(short)\n\n\nasync def test_text_component(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:\n    \"\"\"Test text component with markdown.\"\"\"\n    text_id = str(uuid.uuid4())\n    test_state[\"component_ids\"][\"text\"] = text_id\n\n    # Create with comprehensive markdown\n    text = RichTextComponent(\n        id=text_id,\n        content=\"\"\"# Test Text Component\n\nThis component demonstrates **markdown rendering** with various formatting:\n\n## Formatting Examples\n- **Bold text** for emphasis\n- *Italic text* for style\n- `inline code` for snippets\n- ~~Strikethrough~~ for deletions\n\n### Lists\n1. First ordered item\n2. Second ordered item\n3. Third ordered item\n\n### Code Block\n```python\ndef hello():\n    return \"Markdown works!\"\n```\n\n> Blockquote to test quote rendering\n\nThis validates that markdown is properly parsed and displayed.\"\"\",\n        markdown=True,\n    )\n    yield await yield_chunk(text, conversation_id, request_id)\n    await delay(mode)\n\n    # Update with simpler markdown\n    text_updated = text.update(content=\"\"\"# Updated Text Component\n\nText has been **successfully updated** with new markdown content!\n\n- Update operation works ✓\n- Markdown still renders ✓\"\"\")\n    yield await yield_chunk(text_updated, conversation_id, request_id)\n    await delay(mode)\n\n\nasync def test_status_card(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:\n    \"\"\"Test status card with all states.\"\"\"\n    card_id = str(uuid.uuid4())\n    test_state[\"component_ids\"][\"status_card\"] = card_id\n\n    # Create - pending\n    status_card = StatusCardComponent(\n        id=card_id,\n        title=\"Status Card Test\",\n        status=\"pending\",\n        description=\"Testing status card component...\",\n        icon=\"⏳\",\n        collapsible=True,\n        collapsed=False,\n    )\n    yield await yield_chunk(status_card, conversation_id, request_id)\n    await delay(mode)\n\n    # Update to running\n    status_card_running = status_card.set_status(\"running\", \"Processing test...\")\n    yield await yield_chunk(status_card_running, conversation_id, request_id)\n    await delay(mode)\n\n    # Update to completed\n    status_card_done = status_card.set_status(\"completed\", \"Test completed successfully!\")\n    status_card_done.icon = \"✅\"\n    yield await yield_chunk(status_card_done, conversation_id, request_id)\n    await delay(mode)\n\n\nasync def test_progress_display(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:\n    \"\"\"Test progress display component.\"\"\"\n    progress_id = str(uuid.uuid4())\n    test_state[\"component_ids\"][\"progress_display\"] = progress_id\n\n    # Create at 0%\n    progress = ProgressDisplayComponent(\n        id=progress_id,\n        label=\"Test Progress\",\n        value=0.0,\n        description=\"Starting test...\",\n        status=\"info\",\n        animated=True,\n    )\n    yield await yield_chunk(progress, conversation_id, request_id)\n    await delay(mode, 0.05, 0.3)\n\n    # Update to 50%\n    progress_half = progress.update_progress(0.5, \"Halfway there...\")\n    yield await yield_chunk(progress_half, conversation_id, request_id)\n    await delay(mode, 0.05, 0.3)\n\n    # Update to 100%\n    progress_done = progress.update_progress(1.0, \"Complete!\")\n    progress_done.status = \"success\"\n    yield await yield_chunk(progress_done, conversation_id, request_id)\n    await delay(mode)\n\n\nasync def test_card_component(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:\n    \"\"\"Test card component with actions.\"\"\"\n    card_id = str(uuid.uuid4())\n    test_state[\"component_ids\"][\"card\"] = card_id\n\n    # Create card with markdown content and buttons\n    card = CardComponent(\n        id=card_id,\n        title=\"Test Card with Markdown\",\n        content=\"\"\"# Card Content\n\nThis card demonstrates **markdown rendering** within cards:\n\n- Interactive action buttons\n- Collapsible sections\n- Status indicators\n- `Formatted text`\n\nClick the buttons below to test interactivity!\"\"\",\n        icon=\"🃏\",\n        status=\"info\",\n        markdown=True,\n        collapsible=True,\n        collapsed=False,\n        actions=[\n            {\"label\": \"Test Action\", \"action\": \"/test-action\", \"variant\": \"primary\"},\n            {\"label\": \"Cancel\", \"action\": \"/cancel\", \"variant\": \"secondary\"},\n        ],\n    )\n    yield await yield_chunk(card, conversation_id, request_id)\n    await delay(mode)\n\n    # Update card status and content\n    card_updated = card.update(\n        status=\"success\",\n        content=\"\"\"# Card Updated Successfully!\n\nThe card content has been **updated** with:\n- New status (success)\n- New markdown content\n- Same action buttons\n\n✓ Update operation verified\"\"\",\n        markdown=True\n    )\n    yield await yield_chunk(card_updated, conversation_id, request_id)\n    await delay(mode)\n\n\nasync def test_task_list(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:\n    \"\"\"Test task list component.\"\"\"\n    task_list_id = str(uuid.uuid4())\n    test_state[\"component_ids\"][\"task_list\"] = task_list_id\n\n    # Create task list\n    tasks = [\n        Task(title=\"Setup development environment\", description=\"Install dependencies and configure tools\", status=\"completed\", progress=1.0),\n        Task(title=\"Write test suite\", description=\"Create comprehensive component tests\", status=\"in_progress\", progress=0.7),\n        Task(title=\"Run validation\", description=\"Validate all components render correctly\", status=\"pending\"),\n        Task(title=\"Prune webcomponent\", description=\"Remove unused code and cruft\", status=\"pending\"),\n    ]\n    task_list = TaskListComponent(\n        id=task_list_id,\n        title=\"Webcomponent Validation Workflow\",\n        tasks=tasks,\n        show_progress=True,\n        show_timestamps=True,\n    )\n    yield await yield_chunk(task_list, conversation_id, request_id)\n    await delay(mode)\n\n    # Update task statuses\n    tasks[1].status = \"completed\"\n    tasks[1].progress = 1.0\n    tasks[2].status = \"in_progress\"\n    tasks[2].progress = 0.3\n    task_list_updated = TaskListComponent(\n        id=task_list_id,\n        title=\"Webcomponent Validation Workflow (Updated)\",\n        tasks=tasks,\n        show_progress=True,\n        show_timestamps=True,\n    )\n    task_list_updated.lifecycle = ComponentLifecycle.UPDATE\n    yield await yield_chunk(task_list_updated, conversation_id, request_id)\n    await delay(mode)\n\n\nasync def test_progress_bar(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:\n    \"\"\"Test progress bar component.\"\"\"\n    bar_id = str(uuid.uuid4())\n    test_state[\"component_ids\"][\"progress_bar\"] = bar_id\n\n    # Create\n    bar = ProgressBarComponent(\n        id=bar_id,\n        value=0.3,\n        label=\"Loading\",\n        status=\"info\",\n    )\n    yield await yield_chunk(bar, conversation_id, request_id)\n    await delay(mode, 0.05, 0.2)\n\n    # Update\n    bar_updated = bar.update(value=0.8, status=\"success\")\n    yield await yield_chunk(bar_updated, conversation_id, request_id)\n    await delay(mode)\n\n\nasync def test_notification(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:\n    \"\"\"Test notification component.\"\"\"\n    for level in [\"info\", \"success\", \"warning\", \"error\"]:\n        notif = NotificationComponent(\n            id=str(uuid.uuid4()),\n            message=f\"This is a {level} notification\",\n            level=level,\n            title=f\"{level.capitalize()} Test\",\n        )\n        yield await yield_chunk(notif, conversation_id, request_id)\n        await delay(mode, 0.05, 0.2)\n\n\nasync def test_status_indicator(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:\n    \"\"\"Test status indicator component.\"\"\"\n    indicator_id = str(uuid.uuid4())\n    test_state[\"component_ids\"][\"status_indicator\"] = indicator_id\n\n    # Create with pulse\n    indicator = StatusIndicatorComponent(\n        id=indicator_id,\n        status=\"running\",\n        message=\"Processing...\",\n        pulse=True,\n    )\n    yield await yield_chunk(indicator, conversation_id, request_id)\n    await delay(mode)\n\n    # Update to success\n    indicator_success = indicator.update(status=\"success\", message=\"Done!\", pulse=False)\n    yield await yield_chunk(indicator_success, conversation_id, request_id)\n    await delay(mode)\n\n\nasync def test_badge(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:\n    \"\"\"Test badge component.\"\"\"\n    badge = BadgeComponent(\n        id=str(uuid.uuid4()),\n        text=\"Test Badge\",\n        variant=\"primary\",\n    )\n    yield await yield_chunk(badge, conversation_id, request_id)\n    await delay(mode)\n\n\nasync def test_icon_text(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:\n    \"\"\"Test icon_text component.\"\"\"\n    icon_text = IconTextComponent(\n        id=str(uuid.uuid4()),\n        icon=\"🔧\",\n        text=\"Tool Icon Test\",\n    )\n    yield await yield_chunk(icon_text, conversation_id, request_id)\n    await delay(mode)\n\n\nasync def test_buttons(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:\n    \"\"\"Test button and button_group components.\"\"\"\n    # Single button\n    button = ButtonComponent(\n        label=\"Single Button\",\n        action=\"/button-test\",\n        variant=\"primary\",\n        icon=\"🔘\",\n    )\n    yield await yield_chunk(button, conversation_id, request_id)\n    await delay(mode, 0.05, 0.2)\n\n    # Button group\n    button_group = ButtonGroupComponent(\n        buttons=[\n            {\"label\": \"Option 1\", \"action\": \"/option1\", \"variant\": \"primary\"},\n            {\"label\": \"Option 2\", \"action\": \"/option2\", \"variant\": \"secondary\"},\n            {\"label\": \"Option 3\", \"action\": \"/option3\", \"variant\": \"success\"},\n        ],\n        orientation=\"horizontal\",\n    )\n    yield await yield_chunk(button_group, conversation_id, request_id)\n    await delay(mode)\n\n\nasync def test_dataframe(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:\n    \"\"\"Test dataframe component with sample data.\"\"\"\n    dataframe_id = str(uuid.uuid4())\n    test_state[\"component_ids\"][\"dataframe\"] = dataframe_id\n\n    # Create sample data\n    sample_data = [\n        {\"id\": 1, \"name\": \"Alice\", \"age\": 30, \"city\": \"New York\", \"salary\": 75000},\n        {\"id\": 2, \"name\": \"Bob\", \"age\": 25, \"city\": \"San Francisco\", \"salary\": 85000},\n        {\"id\": 3, \"name\": \"Charlie\", \"age\": 35, \"city\": \"Chicago\", \"salary\": 70000},\n        {\"id\": 4, \"name\": \"Diana\", \"age\": 28, \"city\": \"Boston\", \"salary\": 80000},\n        {\"id\": 5, \"name\": \"Eve\", \"age\": 32, \"city\": \"Seattle\", \"salary\": 90000},\n    ]\n\n    dataframe = DataFrameComponent.from_records(\n        records=sample_data,\n        title=\"📊 Employee Data\",\n        description=\"\"\"Sample employee dataset demonstrating **DataFrame** features:\n\n- **Searchable**: Try searching for names or cities\n- **Sortable**: Click column headers to sort\n- **Exportable**: Export to CSV/Excel\n- **Paginated**: Navigate through rows\n\n*5 employees across different cities*\"\"\",\n        id=dataframe_id,\n        searchable=True,\n        sortable=True,\n        exportable=True,\n    )\n    yield await yield_chunk(dataframe, conversation_id, request_id)\n    await delay(mode)\n\n    # Update with more data\n    updated_data = sample_data + [\n        {\"id\": 6, \"name\": \"Frank\", \"age\": 29, \"city\": \"Austin\", \"salary\": 78000},\n    ]\n    dataframe_updated = DataFrameComponent.from_records(\n        records=updated_data,\n        title=\"📊 Employee Data (Updated)\",\n        description=\"\"\"Dataset **updated** with new employee!\n\n✓ Added Frank from Austin\n✓ Now showing 6 employees\n✓ Update operation verified\"\"\",\n        id=dataframe_id,\n    )\n    dataframe_updated.lifecycle = ComponentLifecycle.UPDATE\n    yield await yield_chunk(dataframe_updated, conversation_id, request_id)\n    await delay(mode)\n\n\nasync def test_chart(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:\n    \"\"\"Test chart component with Plotly data.\"\"\"\n    chart_id = str(uuid.uuid4())\n    test_state[\"component_ids\"][\"chart\"] = chart_id\n\n    # Create a simple bar chart\n    chart_data = {\n        \"data\": [\n            {\n                \"x\": [\"Product A\", \"Product B\", \"Product C\", \"Product D\"],\n                \"y\": [20, 35, 30, 25],\n                \"type\": \"bar\",\n                \"name\": \"Sales\",\n                \"marker\": {\"color\": \"#667eea\"},\n            }\n        ],\n        \"layout\": {\n            \"title\": \"Product Sales\",\n            \"xaxis\": {\"title\": \"Products\"},\n            \"yaxis\": {\"title\": \"Sales (units)\"},\n        },\n    }\n\n    chart = ChartComponent(\n        id=chart_id,\n        chart_type=\"bar\",\n        data=chart_data,\n        title=\"Sales Chart\",\n    )\n    yield await yield_chunk(chart, conversation_id, request_id)\n    await delay(mode)\n\n    # Update to line chart\n    line_chart_data = {\n        \"data\": [\n            {\n                \"x\": [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\"],\n                \"y\": [10, 15, 13, 17, 21],\n                \"type\": \"scatter\",\n                \"mode\": \"lines+markers\",\n                \"name\": \"Revenue\",\n                \"line\": {\"color\": \"#10b981\", \"width\": 3},\n            }\n        ],\n        \"layout\": {\n            \"title\": \"Monthly Revenue Trend\",\n            \"xaxis\": {\"title\": \"Month\"},\n            \"yaxis\": {\"title\": \"Revenue ($1000s)\"},\n        },\n    }\n\n    chart_updated = ChartComponent(\n        id=chart_id,\n        chart_type=\"line\",\n        data=line_chart_data,\n        title=\"Revenue Chart\",\n    )\n    chart_updated.lifecycle = ComponentLifecycle.UPDATE\n    yield await yield_chunk(chart_updated, conversation_id, request_id)\n    await delay(mode)\n\nasync def test_artifact(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:\n    \"\"\"Test artifact component with HTML/SVG content.\"\"\"\n    artifact_id = str(uuid.uuid4())\n    test_state[\"component_ids\"][\"artifact\"] = artifact_id\n\n    # Create SVG artifact\n    svg_content = '''<svg width=\"200\" height=\"200\" xmlns=\"http://www.w3.org/2000/svg\">\n    <circle cx=\"100\" cy=\"100\" r=\"80\" fill=\"#667eea\" opacity=\"0.8\"/>\n    <circle cx=\"100\" cy=\"100\" r=\"60\" fill=\"#764ba2\" opacity=\"0.6\"/>\n    <circle cx=\"100\" cy=\"100\" r=\"40\" fill=\"#f093fb\" opacity=\"0.4\"/>\n    <text x=\"100\" y=\"105\" text-anchor=\"middle\" fill=\"white\" font-size=\"20\" font-weight=\"bold\">\n        Test SVG\n    </text>\n</svg>'''\n\n    artifact = ArtifactComponent(\n        id=artifact_id,\n        content=svg_content,\n        artifact_type=\"svg\",\n        title=\"SVG Circle Visualization\",\n        description=\"Concentric circles demonstration\",\n        fullscreen_capable=True,\n    )\n    yield await yield_chunk(artifact, conversation_id, request_id)\n    await delay(mode)\n\n\nasync def test_log_viewer(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:\n    \"\"\"Test log viewer component.\"\"\"\n    log_id = str(uuid.uuid4())\n    test_state[\"component_ids\"][\"log_viewer\"] = log_id\n\n    # Create initial log viewer with entries\n    log_viewer = LogViewerComponent(\n        id=log_id,\n        title=\"System Logs\",\n        entries=[\n            LogEntry(message=\"System started\", level=\"info\"),\n            LogEntry(message=\"Loading configuration...\", level=\"info\"),\n            LogEntry(message=\"Configuration loaded successfully\", level=\"info\"),\n        ],\n        searchable=True,\n        auto_scroll=True,\n    )\n    yield await yield_chunk(log_viewer, conversation_id, request_id)\n    await delay(mode, 0.05, 0.3)\n\n    # Add warning\n    log_viewer = log_viewer.add_entry(\"Memory usage at 75%\", level=\"warning\")\n    yield await yield_chunk(log_viewer, conversation_id, request_id)\n    await delay(mode, 0.05, 0.3)\n\n    # Add error\n    log_viewer = log_viewer.add_entry(\"Connection timeout\", level=\"error\", data={\"host\": \"api.example.com\", \"port\": 443})\n    yield await yield_chunk(log_viewer, conversation_id, request_id)\n    await delay(mode, 0.05, 0.3)\n\n    # Add success\n    log_viewer = log_viewer.add_entry(\"Reconnected successfully\", level=\"info\")\n    yield await yield_chunk(log_viewer, conversation_id, request_id)\n    await delay(mode)\n\nasync def test_ui_state_updates(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:\n    \"\"\"Test UI state update components.\"\"\"\n    # Status bar update\n    status_bar = StatusBarUpdateComponent(\n        message=\"Running comprehensive component test...\",\n        status=\"info\",\n    )\n    yield await yield_chunk(status_bar, conversation_id, request_id)\n    await delay(mode, 0.1, 0.3)\n\n    # Task tracker - add tasks to sidebar\n    task1 = Task(\n        title=\"Validate Text Components\",\n        description=\"Test text, markdown, and formatting\",\n        status=\"completed\",\n        progress=1.0,\n    )\n    task_tracker_add1 = TaskTrackerUpdateComponent.add_task(task1)\n    yield await yield_chunk(task_tracker_add1, conversation_id, request_id)\n    await delay(mode, 0.1, 0.3)\n\n    task2 = Task(\n        title=\"Validate Data Components\",\n        description=\"Test DataFrame, Chart, Code blocks\",\n        status=\"in_progress\",\n        progress=0.6,\n    )\n    task_tracker_add2 = TaskTrackerUpdateComponent.add_task(task2)\n    yield await yield_chunk(task_tracker_add2, conversation_id, request_id)\n    await delay(mode, 0.1, 0.3)\n\n    task3 = Task(\n        title=\"Validate Interactive Components\",\n        description=\"Test buttons, actions, and UI state\",\n        status=\"pending\",\n    )\n    task_tracker_add3 = TaskTrackerUpdateComponent.add_task(task3)\n    yield await yield_chunk(task_tracker_add3, conversation_id, request_id)\n    await delay(mode, 0.1, 0.3)\n\n    # Update task 2 to completed\n    task_tracker_update = TaskTrackerUpdateComponent(\n        operation=TaskOperation.UPDATE_TASK,\n        task_id=task2.id,\n        status=\"completed\",\n        progress=1.0,\n    )\n    yield await yield_chunk(task_tracker_update, conversation_id, request_id)\n    await delay(mode, 0.1, 0.3)\n\n    # Update status bar\n    status_bar_complete = StatusBarUpdateComponent(\n        message=\"All components validated successfully!\",\n        status=\"success\",\n    )\n    yield await yield_chunk(status_bar_complete, conversation_id, request_id)\n    await delay(mode, 0.1, 0.3)\n\n    # Chat input update - change placeholder\n    chat_input = ChatInputUpdateComponent(\n        placeholder=\"Type a message to test chat input updates...\",\n        disabled=False,\n    )\n    yield await yield_chunk(chat_input, conversation_id, request_id)\n    await delay(mode)\n\n\nasync def run_comprehensive_test(conversation_id: str, request_id: str, mode: str) -> AsyncGenerator[ChatStreamChunk, None]:\n    \"\"\"Run all component tests.\"\"\"\n    # Introduction\n    intro = RichTextComponent(\n        content=f\"\"\"# 🧪 Comprehensive Component Test\n\n**Mode**: {mode}\n\n## Test Coverage\nThis test validates **16 component types** supported by the webcomponent:\n- ✅ Component creation\n- ✅ Incremental updates\n- ✅ Markdown rendering\n- ✅ Interactive actions\n- ✅ Data visualization\n\n### Component Categories\n1. **Primitive**: Text, Badge, Icon Text\n2. **Feedback**: Status Card, Progress, Notifications, Logs\n3. **Data**: Card, Task List, DataFrame, Chart, Code\n4. **Specialized**: Artifact (SVG/HTML)\n5. **Interactive**: Buttons with actions\n\nWatch the sidebar checklist as components render! ➡️\"\"\",\n        markdown=True,\n    )\n    yield await yield_chunk(intro, conversation_id, request_id)\n    await delay(mode)\n\n    # Run all tests\n    async for chunk in test_text_component(conversation_id, request_id, mode):\n        yield chunk\n\n    async for chunk in test_status_card(conversation_id, request_id, mode):\n        yield chunk\n\n    async for chunk in test_progress_display(conversation_id, request_id, mode):\n        yield chunk\n\n    async for chunk in test_card_component(conversation_id, request_id, mode):\n        yield chunk\n\n    async for chunk in test_task_list(conversation_id, request_id, mode):\n        yield chunk\n\n    async for chunk in test_progress_bar(conversation_id, request_id, mode):\n        yield chunk\n\n    async for chunk in test_notification(conversation_id, request_id, mode):\n        yield chunk\n\n    async for chunk in test_status_indicator(conversation_id, request_id, mode):\n        yield chunk\n\n    async for chunk in test_badge(conversation_id, request_id, mode):\n        yield chunk\n\n    async for chunk in test_icon_text(conversation_id, request_id, mode):\n        yield chunk\n\n    async for chunk in test_buttons(conversation_id, request_id, mode):\n        yield chunk\n\n    async for chunk in test_dataframe(conversation_id, request_id, mode):\n        yield chunk\n\n    async for chunk in test_chart(conversation_id, request_id, mode):\n        yield chunk\n\n    async for chunk in test_artifact(conversation_id, request_id, mode):\n        yield chunk\n\n    async for chunk in test_log_viewer(conversation_id, request_id, mode):\n        yield chunk\n\n    # NOTE: Table, Container, and CodeBlock components are defined in vanna Python package\n    # but NOT supported by the webcomponent (no renderers). Skipping these tests.\n    # These are candidates for removal from the vanna package.\n\n    async for chunk in test_ui_state_updates(conversation_id, request_id, mode):\n        yield chunk\n\n    # Completion message\n    done = StatusCardComponent(\n        title=\"✅ Test Suite Complete\",\n        status=\"completed\",\n        description=f\"\"\"All **16 component types** successfully rendered in **{mode}** mode!\n\n**Validated:**\n- Component creation & updates\n- Markdown rendering\n- Interactive buttons\n- Data visualization\n- UI state management\n\nCheck the sidebar for the complete checklist.\"\"\",\n        icon=\"✅\",\n    )\n    yield await yield_chunk(done, conversation_id, request_id)\n\n\nasync def handle_action_message(message: str, conversation_id: str, request_id: str) -> AsyncGenerator[ChatStreamChunk, None]:\n    \"\"\"Handle button action messages.\"\"\"\n    test_state[\"action_count\"] += 1\n\n    response = NotificationComponent(\n        message=f\"Action received: {message}\",\n        level=\"success\",\n        title=f\"Action #{test_state['action_count']}\",\n    )\n    yield await yield_chunk(response, conversation_id, request_id)\n\n    # Also show a card with details\n    card = CardComponent(\n        title=\"Action Handler Response\",\n        content=f\"Received action: `{message}`\\n\\nThis confirms button interactivity is working!\",\n        icon=\"🎯\",\n        status=\"success\",\n    )\n    yield await yield_chunk(card, conversation_id, request_id)\n\n\n# FastAPI app\napp = FastAPI(title=\"Vanna Webcomponent Test Backend\")\n\n# CORS\napp.add_middleware(\n    CORSMiddleware,\n    allow_origins=[\"*\"],\n    allow_credentials=True,\n    allow_methods=[\"*\"],\n    allow_headers=[\"*\"],\n)\n\n# Mount static files (static directory for webcomponent)\nstatic_path = os.path.join(os.path.dirname(__file__), \"static\")\nif os.path.exists(static_path):\n    app.mount(\"/static\", StaticFiles(directory=static_path), name=\"static\")\n\n\n@app.post(\"/api/vanna/v2/chat_sse\")\nasync def chat_sse(chat_request: ChatRequest) -> StreamingResponse:\n    \"\"\"SSE endpoint for streaming chat.\"\"\"\n    conversation_id = chat_request.conversation_id or str(uuid.uuid4())\n    request_id = chat_request.request_id or str(uuid.uuid4())\n    message = chat_request.message.strip()\n\n    async def generate() -> AsyncGenerator[str, None]:\n        \"\"\"Generate SSE stream.\"\"\"\n        try:\n            # Handle button actions\n            if message.startswith(\"/\") and message != \"/test\":\n                async for chunk in handle_action_message(message, conversation_id, request_id):\n                    yield f\"data: {chunk.model_dump_json()}\\n\\n\"\n\n            # Handle test command or initial message\n            elif message == \"/test\" or \"test\" in message.lower():\n                async for chunk in run_comprehensive_test(conversation_id, request_id, test_state[\"mode\"]):\n                    yield f\"data: {chunk.model_dump_json()}\\n\\n\"\n\n            # Default response\n            else:\n                response = RichTextComponent(\n                    content=f\"You said: {message}\\n\\nType `/test` to run the comprehensive component test.\",\n                    markdown=True,\n                )\n                chunk = await yield_chunk(response, conversation_id, request_id)\n                yield f\"data: {chunk.model_dump_json()}\\n\\n\"\n\n            yield \"data: [DONE]\\n\\n\"\n\n        except Exception as e:\n            error_message = f\"{str(e)}\\n\\nTraceback:\\n{traceback.format_exc()}\"\n            print(f\"ERROR in chat_sse: {error_message}\")  # Log to console\n            error_chunk = {\n                \"type\": \"error\",\n                \"data\": {\"message\": error_message},\n                \"conversation_id\": conversation_id,\n                \"request_id\": request_id,\n            }\n            yield f\"data: {json.dumps(error_chunk)}\\n\\n\"\n\n    return StreamingResponse(\n        generate(),\n        media_type=\"text/event-stream\",\n        headers={\n            \"Cache-Control\": \"no-cache\",\n            \"Connection\": \"keep-alive\",\n            \"X-Accel-Buffering\": \"no\",\n        },\n    )\n\n\n@app.get(\"/health\")\nasync def health():\n    \"\"\"Health check.\"\"\"\n    return {\"status\": \"ok\", \"mode\": test_state[\"mode\"]}\n\n\n@app.get(\"/\")\nasync def root():\n    \"\"\"Serve test HTML page.\"\"\"\n    html_path = os.path.join(os.path.dirname(__file__), \"test-comprehensive.html\")\n    if os.path.exists(html_path):\n        return FileResponse(html_path)\n    return {\n        \"message\": \"Vanna Webcomponent Test Backend\",\n        \"mode\": test_state[\"mode\"],\n        \"endpoints\": {\n            \"chat\": \"POST /api/vanna/v2/chat_sse\",\n            \"health\": \"GET /health\",\n        },\n    }\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(description=\"Test backend for vanna-webcomponent\")\n    parser.add_argument(\n        \"--mode\",\n        choices=[\"rapid\", \"realistic\"],\n        default=\"realistic\",\n        help=\"Test mode: rapid (fast) or realistic (with delays)\",\n    )\n    parser.add_argument(\"--host\", default=\"0.0.0.0\", help=\"Host to bind to\")\n    parser.add_argument(\"--port\", type=int, default=5555, help=\"Port to bind to\")\n\n    args = parser.parse_args()\n    test_state[\"mode\"] = args.mode\n\n    print(f\"Starting test backend in {args.mode} mode...\")\n    print(f\"Server running at http://{args.host}:{args.port}\")\n    print(\"Send message '/test' to run comprehensive component test\")\n\n    import uvicorn\n    uvicorn.run(app, host=args.host, port=args.port)\n"
  },
  {
    "path": "frontends/webcomponent/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"experimentalDecorators\": true,\n    \"useDefineForClassFields\": false\n  },\n  \"include\": [\"src\"]\n}"
  },
  {
    "path": "frontends/webcomponent/vite.config.ts",
    "content": "import { defineConfig } from 'vite';\n\nexport default defineConfig({\n  define: {\n    __BUILD_TIME__: JSON.stringify(new Date().toISOString()),\n    __BUILD_VERSION__: JSON.stringify(process.env.npm_package_version || '1.0.0'),\n  },\n  build: {\n    outDir: 'dist',\n    lib: {\n      entry: 'src/index.ts',\n      formats: ['es'],\n      fileName: () => 'vanna-components.js',\n    },\n    rollupOptions: {\n      // Remove external to bundle lit with the components\n      // external: /^lit/,\n    },\n  },\n  preview: {\n    port: 9876,\n    strictPort: true,\n  },\n});"
  },
  {
    "path": "notebooks/quickstart.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Install the Package\\n\",\n    \"Here we're installing it directly from GitHub while it's in development.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"!pip install 'vanna[flask,anthropic]'\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Download a Sample Database\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import httpx\\n\",\n    \"\\n\",\n    \"with open(\\\"Chinook.sqlite\\\", \\\"wb\\\") as f:\\n\",\n    \"    with httpx.stream(\\\"GET\\\", \\\"https://vanna.ai/Chinook.sqlite\\\") as response:\\n\",\n    \"        for chunk in response.iter_bytes():\\n\",\n    \"            f.write(chunk)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Imports\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from vanna import Agent, AgentConfig\\n\",\n    \"from vanna.servers.fastapi import VannaFastAPIServer\\n\",\n    \"from vanna.core.registry import ToolRegistry\\n\",\n    \"from vanna.core.user import UserResolver, User, RequestContext\\n\",\n    \"from vanna.integrations.anthropic import AnthropicLlmService\\n\",\n    \"from vanna.tools import RunSqlTool, VisualizeDataTool\\n\",\n    \"from vanna.integrations.sqlite import SqliteRunner\\n\",\n    \"from vanna.tools.agent_memory import SaveQuestionToolArgsTool, SearchSavedCorrectToolUsesTool\\n\",\n    \"from vanna.integrations.local.agent_memory import DemoAgentMemory\\n\",\n    \"from vanna.capabilities.sql_runner import RunSqlToolArgs\\n\",\n    \"from vanna.tools.visualize_data import VisualizeDataArgs\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Define your User Authentication\\n\",\n    \"Here we're going to say that if you're logged in as `admin@example.com` then you're in the `admin` group, otherwise you're in the `user` group\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"class SimpleUserResolver(UserResolver):\\n\",\n    \"    async def resolve_user(self, request_context: RequestContext) -> User:\\n\",\n    \"        # In production, validate cookies/JWTs here\\n\",\n    \"        user_email = request_context.get_cookie('vanna_email')\\n\",\n    \"        if not user_email:\\n\",\n    \"            raise ValueError(\\\"Missing 'vanna_email' cookie for user identification\\\")\\n\",\n    \"        \\n\",\n    \"        print(f\\\"Resolving user for email: {user_email}\\\")\\n\",\n    \"\\n\",\n    \"        if user_email == \\\"admin@example.com\\\":\\n\",\n    \"            return User(id=\\\"admin1\\\", email=user_email, group_memberships=['admin'])\\n\",\n    \"        \\n\",\n    \"        return User(id=\\\"user1\\\", email=user_email, group_memberships=['user'])\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Define the Tools and Access Control\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"tools = ToolRegistry()\\n\",\n    \"tools.register_local_tool(RunSqlTool(sql_runner=SqliteRunner(database_path=\\\"./Chinook.sqlite\\\")), access_groups=['admin', 'user'])\\n\",\n    \"tools.register_local_tool(VisualizeDataTool(), access_groups=['admin', 'user'])\\n\",\n    \"agent_memory = DemoAgentMemory(max_items=1000)\\n\",\n    \"tools.register_local_tool(SaveQuestionToolArgsTool(), access_groups=['admin'])\\n\",\n    \"tools.register_local_tool(SearchSavedCorrectToolUsesTool(), access_groups=['admin', 'user'])\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"# Set up LLM\\n\",\n    \"llm = AnthropicLlmService(model=\\\"claude-sonnet-4-5\\\", api_key=\\\"sk-ant-...\\\")\\n\",\n    \"\\n\",\n    \"# Create agent with your options\\n\",\n    \"agent = Agent(\\n\",\n    \"    llm_service=llm,\\n\",\n    \"    tool_registry=tools,\\n\",\n    \"    user_resolver=SimpleUserResolver(),\\n\",\n    \"    config=AgentConfig(),\\n\",\n    \"    agent_memory=agent_memory\\n\",\n    \")\\n\",\n    \"\\n\",\n    \"# 4. Create and run server\\n\",\n    \"server = VannaFastAPIServer(agent)\\n\",\n    \"server.run()\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": []\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \"venv\",\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.13.5\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 2\n}\n"
  },
  {
    "path": "papers/ai-sql-accuracy-2023-08-17.md",
    "content": "# AI SQL Accuracy: Testing different LLMs + context strategies to maximize SQL generation accuracy\n_2023-08-17_\n\n## TLDR\n\nThe promise of having an autonomous AI agent that can answer business users’ plain English questions is an attractive but thus far elusive proposition. Many have tried, with limited success, to get ChatGPT to write. The failure is primarily due of a lack of the LLM's knowledge of the particular dataset it’s being asked to query.\n\nIn this paper, **we show that context is everything, and with the right context, we can get from ~3% accuracy to ~80% accuracy**. We go through three different context strategies, and showcase one that is the clear winner - where we combine schema definitions, documentation, and prior SQL queries with a relevance search.\n\nWe also compare a few different LLMs - including Google Bison, GPT 3.5, GPT 4, and a brief attempt with Llama 2. While **GPT 4 takes the crown of the best overall LLM for generating SQL**, Google’s Bison is roughly equivalent when enough context is provided.\n\nFinally, we show how you can use the methods demonstrated here to generate SQL for your database.\n\nHere's a summary of our key findings -\n\n![](https://raw.githubusercontent.com/vanna-ai/vanna/main/papers/img/summary.png)\n\n## Table of Contents\n* [Why use AI to generate SQL?](#why-use-ai-to-generate-sql)\n* [Setting up architecture of the test](#setting-up-architecture-of-the-test)\n* [Setting up the test levers](#setting-up-the-test-levers)\n    * [Choosing a dataset](#choosing-a-dataset)\n    * [Choosing the questions](#choosing-the-questions)\n    * [Choosing the prompt](#choosing-the-prompt)\n    * [Choosing the LLMs (Foundational models)](#choosing-the-llms-foundational-models)\n    * [Choosing the context](#choosing-the-context)\n* [Using ChatGPT to generate SQL](#using-chatgpt-to-generate-sql)\n* [Using schema only](#using-schema-only)\n* [Using SQL examples](#using-sql-examples)\n* [Using contextually relevant examples](#using-contextually-relevant-examples)\n* [Analyzing the results](#analyzing-the-results)\n* [Next steps to getting accuracy even higher](#next-steps-to-getting-accuracy-even-higher)\n* [Use AI to write SQL for your dataset](#use-ai-to-write-sql-for-your-dataset)\n\n\n\n## Why use AI to generate SQL?\n\nMany organizations have now adopted some sort of data warehouse or data lake - a repository of a lot of the organization’s critical data that is queryable for analytical purposes. This ocean of data is brimming with potential insights, but only a small fraction of people in an enterprise have the two skills required to harness the data —\n\n1. A solid comprehension of **advanced SQL**, and\n2. A comprehensive knowledge of the **organization’s unique data structure & schema**\n\nThe number of people with both of the above is not only vanishingly small, but likely not the same people that have the majority of the questions. \n\n**So what actually happens inside organizations?** Business users, like product managers, sales managers, and executives, have data questions that will inform business decisions and strategy. They’ll first check dashboards, but most questions are ad hoc and specific, and the answers aren’t available, so they’ll ask a data analyst or engineer - whomever possesses the combination of skills above. These people are busy, and take a while to get to the request, and as soon as they get an answer, the business user has follow up questions. \n\n**This process is painful** for both the business user (long lead times to get answers) and the analyst (distracts from their main projects), and leads to many potential insights being lost.\n\n![](https://raw.githubusercontent.com/vanna-ai/vanna/main/papers/img/question-flow.png)\n\n**Enter generative AI!** LLMs potentially give the opportunity to business users to query the database in plain English (with the LLMs doing the SQL translation), and we have heard from dozens of companies that this would be a game changer for their data teams and even their businesses.\n\n**The key challenge is generating accurate SQL for complex and messy databases**. Plenty of people we’ve spoken with have tried to use ChatGPT to write SQL with limited success and a lot of pain. Many have given up and reverted back to the old fashioned way of manually writing SQL. At best, ChatGPT is a sometimes useful co-pilot for analysts to get syntax right.\n\n**But there’s hope!** We’ve spent the last few months immersed in this problem, trying various models, techniques and approaches to improve the accuracy of SQL generated by LLMs. In this paper, we show the performance of various LLMs and how the strategy of providing contextually relevant correct SQL to the LLM can allow the LLM to **achieve extremely high accuracy**.\n\n\n## Setting up architecture of the test\n\nFirst, we needed to define the architecture of the test. A rough outline is below, in a five step process, with _pseudo code_ below - \n\n![](https://raw.githubusercontent.com/vanna-ai/vanna/main/papers/img/test-architecture.png)\n\n1. **Question** - We start with the business question.\n```python\n   question = \"how many clients are there in germany\"\n```\n2. **Prompt** - We create the prompt to send to the LLM.\n```python\n   prompt = f\"\"\"\n   Write a SQL statement for the following question:\n   {question}\n   \"\"\"\n```\n3. **Generate SQL** - Using an API, we’ll send the prompt to the LLM and get back generated SQL.\n```python\n   sql = llm.api(api_key=api_key, prompt=prompt, parameters=parameters)\n```\n4. **Run SQL** - We'll run the SQL against the database.\n```python\n    df = db.conn.execute(sql)\n```\n5. **Validate results** - Finally, we’ll validate that the results are in line with what we expect.\nThere are some shades of grey when it comes to the results so we did a manual evaluation of the results. You can see those results [here](https://github.com/vanna-ai/research/blob/main/data/sec_evaluation_data_tagged.csv)\n\n## Setting up the test levers\n\nNow that we have our experiment set up, we’ll need to figure out what levers would impact accuracy, and what our test set would be. We tried two levers (the LLMs and the training data used), and we ran on 20 questions that made up our test set. So we ran a total of 3 LLMs x 3 context strategies x 20 questions = 180 individual trials in this experiment.\n\n![](https://raw.githubusercontent.com/vanna-ai/vanna/main/papers/img/test-levers.png)\n\n\n### Choosing a dataset\n\nFirst, we need to **choose an appropriate dataset** to try. We had a few guiding principles - \n\n1. **Representative**. Datasets in enterprises are often complex and this complexity isn’t captured in many demo / sample datasets. We want to use a complicated database that has real-word use cases that contains real-world data. \n2. **Accessible**. We also wanted that dataset to be publicly available. \n3. **Understandable**. The dataset should be somewhat understandable to a wide audience - anything too niche or technical would be difficult to decipher.\n4. **Maintained**. We’d prefer a dataset that’s maintained and updated properly, in reflection of a real database.\n\nA dataset that we found that met the criteria above was the Cybersyn SEC filings dataset, which is available for free on the Snowflake marketplace: \n\nhttps://docs.cybersyn.com/our-data-products/economic-and-financial/sec-filings\n\n\n### Choosing the questions\n\nNext, we need to **choose the questions**. Here are some sample questions (see them all in this [file](https://github.com/vanna-ai/research/blob/main/data/questions_sec.csv)) - \n\n1. How many companies are there in the dataset?\n2. What annual measures are available from the 'ALPHABET INC.' Income Statement?\n3. What are the quarterly 'Automotive sales' and 'Automotive leasing' for Tesla?\n4. How many Chipotle restaurants are there currently?\n\nNow that we have the dataset + questions, we’ll need to come up with the levers. \n\n### Choosing the prompt\n\nFor the **prompt**, for this run, we are going to hold the prompt constant, though we’ll do a follow up which varies the prompt.\n\n### Choosing the LLMs (Foundational models)\n\nFor the **LLMs** to test, we’ll try the following - \n\n1. [**Bison (Google)**](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/models) - Bison is the version of [PaLM 2](https://blog.google/technology/ai/google-palm-2-ai-large-language-model/) that’s available via GCP APIs.\n2. [**GPT 3.5 Turbo (OpenAI)**](https://platform.openai.com/docs/models/gpt-3-5) - GPT 3.5 until recently was the flagship OpenAI model despite 4 being available because of latency and cost benefits, and not a huge accuracy difference (well - we’ll put that to the test) especially for basic tasks.\n3. [**GPT 4 (OpenAI)**](https://platform.openai.com/docs/models/gpt-4) - The more advanced but less performant OpenAI model. GPT 4 is multi-modal, though we won’t be using that functionality.\n4. [**Llama 2 (Meta)**](https://ai.meta.com/llama/) - We really wanted to include an open source model - and the leading one is Meta’s Llama 2. But our setup through [Replicate](https://replicate.com/replicate/llama-2-70b-chat) quickly conked out, and we weren’t able to fix it in time for publishing this. In our early runs, when it did work, we found performance to be mediocre at best.\n\n\n### Choosing the context\n\nFinally, we’ll have three types of **context**. Context refers to what we send to the LLM that helps give the LLM context on our specific dataset.\n\n1. **Schema only**. We put the schema (using DDL) in the context window.\n2. **Static examples**. We put static example SQL queries in the context windows.\n3. **Contextually relevant examples**. Finally, we put the most relevant context (SQL / DDL / documentation) into the context window, finding it via a vector search based on embeddings.\n\n\n## Using ChatGPT to generate SQL\n\nThis section is a bit unfair to ChatGPT, but it’s a helpful place to get started. Let’s just take a question and ask ChatGPT to generate the SQL - \n\n**Prompt**\n\n```\nWrite Snowflake SQL to answer the following question -\n\nWhat are the quarterly revenues by business segment for Alphabet?\n\nReturn SQL only\n```\n\n**Response**\n\nCertainly, here's the SQL query:\n\n```sql\nSELECT\n  EXTRACT(QUARTER FROM date_column) AS quarter,\n  business_segment,\n  SUM(revenue) AS quarterly_revenue\nFROM\n  revenue_table\nWHERE\n  company_name = 'Alphabet'\nGROUP BY\n  quarter,\n  business_segment\nORDER BY\n  quarter,\n  business_segment;\n\n-- Replace date_column, business_segment, revenue, revenue_table, and adjust any other identifiers to match your specific database schema.\n```\n\nInevitably, when we try to run this, we hit an error, because it made up the table name since we didn't provide it - \n\n![](https://raw.githubusercontent.com/vanna-ai/vanna/main/papers/img/sql-error.png)\n\nOf course, we are being unfair to the LLMs - as magical as they are, they cannot (unfortunately? luckily?) possibly know what’s in our database - yet. So let’s hop into the tests where we give more context.\n\n\n## Using schema only\n\nFirst, we take the schema of the dataset and put it into the context window. This is usually what we've seen people do with ChatGPT or in tutorials.\n\nAn example prompt may look like this (in reality we used the information schema because of how Snowflake shares work but this shows the principle) - \n\n```\nThe user provides a question and you provide SQL. You will only respond with SQL code and not with any explanations.\n\nRespond with only SQL code. Do not answer with any explanations -- just the code.\n\nYou may use the following DDL statements as a reference for what tables might be available.\n\nCREATE TABLE Table1...\n\nCREATE TABLE Table2...\n\nCREATE TABLE Table3...\n```\n\nThe results were, in a word, terrible. Of the 60 attempts (20 questions x 3 models), only two questions were answered correctly (both by GPT 4), **for an abysmal accuracy rate of 3%**. Here are the two questions that GPT 4 managed to get right - \n\n1. What are the top 10 measure descriptions by frequency?\n2. What are the distinct statements in the report attributes?\n\n![](https://raw.githubusercontent.com/vanna-ai/vanna/main/papers/img/accuracy-using-schema-only.png)\n\nIt’s evident that by just using the schema, we don’t get close to meeting the bar of a helpful AI SQL agent, though it may be somewhat useful in being an analyst copilot.\n\n\n## Using SQL examples\n\nIf we put ourselves in the shoes of a human who’s exposed to this dataset for the first time, in addition to the table definitions, they’d first look at the example queries to see _how_ to query the database correctly.\n\nThese queries can give additional context not available in the schema - for example, which columns to use, how tables join together, and other intricacies of querying that particular dataset.\n\nCybersyn, as with other data providers on the Snowflake marketplace, provides a few (in this case 3) example queries in their documentation. Let’s include these in the context window.\n\nBy providing just those 3 example queries, we see substantial improvements to the correctness of the SQL generated. However, this accuracy greatly varies by the underlying LLM. It seems that GPT-4 is the most able to generalize the example queries in a way that generates the most accurate SQL.\n\n![](https://raw.githubusercontent.com/vanna-ai/vanna/main/papers/img/accuracy-using-static-examples.png)\n\n## Using contextually relevant examples\n\nEnterprise data warehouses often contain 100s (or even 1000s) of tables, and an order of magnitude more queries that cover all the use cases within their organizations. Given the limited size of the context windows of modern LLMs, we can’t just shove all the prior queries and schema definitions into the prompt.\n\nOur final approach to context is a more sophisticated ML approach - load embeddings of prior queries and the table schemas into a vector database, and only choose the most relevant queries / tables to the question asked. Here's a diagram of what we are doing - note the contextual relevance search in the green box -\n\n![](https://raw.githubusercontent.com/vanna-ai/vanna/main/papers/img/using-contextually-relevant-examples.png)\n\nBy surfacing the most relevant examples of those SQL queries to the LLM, we can drastically improve performance of even the less capable LLMs. Here, we give the LLM the 10 most relevant SQL query examples for the question (from a list of 30 examples stored), and accuracy rates skyrocket.\n\n![](https://raw.githubusercontent.com/vanna-ai/vanna/main/papers/img/accuracy-using-contextual-examples.png)\n\nWe can improve performance even more by maintaining a history of SQL statements that were executable and correctly answer actual questions that users have had.\n\n\n## Analyzing the results\n\nIt’s clear that the biggest difference is not in the type of LLM, but rather in the strategy employed to give the appropriate context to the LLM (eg the “training data” used).\n\n![](https://raw.githubusercontent.com/vanna-ai/vanna/main/papers/img/summary-table.png)\n\nWhen looking at SQL accuracy by context strategy, it’s clear that this is what makes the difference. We go from ~3% accurate using just the schema, to ~80% accurate when intelligently using contextual examples.\n\n![](https://raw.githubusercontent.com/vanna-ai/vanna/main/papers/img/summary.png)\n\nThere are still interesting trends with the LLMs themselves. While Bison starts out at the bottom of the heap in both the Schema and Static context strategies, it rockets to the top with a full Contextual strategy. Averaged across the three strategies, **GPT 4 takes the crown as the best LLM for SQL generation**.\n\n![](https://raw.githubusercontent.com/vanna-ai/vanna/main/papers/img/accuracy-by-llm.png)\n\n## Next steps to getting accuracy even higher\n\nWe'll soon do a follow up on this analysis to get even deeper into accurate SQL generation. Some next steps are -\n\n1. **Use other datasets**: We'd love to try this on other, real world, enterprise datasets. What happens when you get to 100 tables? 1000 tables?\n2. **Add more training data**: While 30 queries is great, what happens when you 10x, 100x that number?\n3. **Try more databases**: This test was run on a Snowflake database, but we've also gotten this working on BigQuery, Postgres, Redshift, and SQL Server.\n4. **Experiment with more foundational models:** We are close to being able to use Llama 2, and we'd love to try other LLMs.\n\nWe have some anecdotal evidence for the above but we'll be expanding and refining our tests to include more of these items.\n\n## Use AI to write SQL for your dataset\n\nWhile the SEC data is a good start, you must be wondering whether this could be relevant for your data and your organization. We’re building a [Python package](https://vanna.ai) that can generate SQL for your database as well as additional functionality like being able to generate Plotly code for the charts, follow-up questions, and various other functions.\n\nHere's an overview of how it works\n```python\nimport vanna as vn\n```\n\n1. **Train Using Schema**\n\n```python\nvn.train(ddl=\"CREATE TABLE ...\")\n```\n\n2. **Train Using Documentation**\n\n```python\nvn.train(documentation=\"...\")\n```\n\n3. **Train Using SQL Examples**\n\n```python\nvn.train(sql=\"SELECT ...\")\n```\n\n4. **Generating SQL**\n\nThe easiest ways to use Vanna out of the box are `vn.ask(question=\"What are the ...\")` which will return the SQL, table, and chart as you can see in this [example notebook](https://vanna.ai/docs/getting-started.html). `vn.ask` is a wrapper around `vn.generate_sql`, `vn.run_sql`, `vn.generate_plotly_code`, `vn.get_plotly_figure`, and `vn.generate_followup_questions`. This will use optimized context to generate SQL for your question where Vanna will call the LLM for you.\n\nAlternately, you can use `vn.get_related_training_data(question=\"What are the ...\")` as shown in this [notebook](https://github.com/vanna-ai/research/blob/main/notebooks/test-cybersyn-sec.ipynb) which will retrieve the most relevant context that you can use to construct your own prompt to send to any LLM.\n\nThis [notebook](https://github.com/vanna-ai/research/blob/main/notebooks/train-cybersyn-sec-3.ipynb) shows an example of how the \"Static\" context strategy was used to train Vanna on the Cybersyn SEC dataset.\n\n## A note on nomenclature\n* **Foundational Model**: This is the underlying LLM\n* **Context Model (aka Vanna Model)**: This is a layer that sits on top of the LLM and provides context to the LLM\n* **Training**: Generally when we refer to \"training\" we're talking about training the context model.\n\n## Contact Us\nPing us on [Slack](https://join.slack.com/t/vanna-ai/shared_invite/zt-1unu0ipog-iE33QCoimQiBDxf2o7h97w), [Discord](https://discord.com/invite/qUZYKHremx), or [set up a 1:1 call](https://calendly.com/d/y7j-yqq-yz4/meet-with-both-vanna-co-founders) if you have any issues.\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"flit_core >=3.2,<4\"]\nbuild-backend = \"flit_core.buildapi\"\n\n[project]\nname = \"vanna\"\nversion = \"2.0.2\"\nauthors = [\n  { name=\"Zain Hoda\", email=\"zain@vanna.ai\" },\n]\n\ndescription = \"Generate SQL queries from natural language\"\nreadme = \"README.md\"\nrequires-python = \">=3.9\"\nclassifiers = [\n    \"Programming Language :: Python :: 3\",\n    \"License :: OSI Approved :: MIT License\",\n    \"Operating System :: OS Independent\",\n]\ndependencies = [\n    \"pydantic>=2.0.0\",\n    \"click>=8.0.0\",\n    \"pandas\",\n    \"httpx>=0.28.0\",\n    \"PyYAML\",\n    \"plotly\",\n    \"tabulate\",\n    \"sqlparse\",\n    \"sqlalchemy\",\n    \"requests\",\n]\n\n[project.scripts]\nvanna = \"vanna.servers.cli.server_runner:main\"\n\n[project.urls]\n\"Homepage\" = \"https://github.com/vanna-ai/vanna\"\n\"Bug Tracker\" = \"https://github.com/vanna-ai/vanna/issues\"\n\n[project.optional-dependencies]\nflask = [\"flask>=2.0.0\", \"flask-cors>=4.0.0\"]\nfastapi = [\"fastapi>=0.68.0\", \"uvicorn>=0.15.0\"]\nservers = [\"vanna[flask,fastapi]\"]\n\npostgres = [\"psycopg2-binary\", \"db-dtypes\"]\nmysql = [\"PyMySQL\"]\nclickhouse = [\"clickhouse_connect\"]\nbigquery = [\"google-cloud-bigquery\"]\nsnowflake = [\"snowflake-connector-python\"]\nduckdb = [\"duckdb\"]\ngoogle = [\"google-generativeai\", \"google-cloud-aiplatform\"]\nall = [\"psycopg2-binary\", \"db-dtypes\", \"PyMySQL\", \"google-cloud-bigquery\", \"snowflake-connector-python\", \"duckdb\", \"openai\", \"qianfan\", \"mistralai>=1.0.0\", \"chromadb>=1.1.0\", \"anthropic\", \"zhipuai\", \"marqo\", \"google-generativeai\", \"google-cloud-aiplatform\", \"qdrant-client>=1.0.0\", \"fastembed\", \"ollama\", \"httpx\", \"opensearch-py\", \"opensearch-dsl\", \"transformers\", \"pinecone\", \"pymilvus[model]\",\"weaviate-client\", \"azure-search-documents\", \"azure-identity\", \"azure-common\", \"faiss-cpu\", \"boto\", \"boto3\", \"botocore\", \"langchain_core\", \"langchain_postgres\", \"langchain-community\", \"langchain-huggingface\", \"xinference-client\"]\ntest = [\"pytest>=7.0.0\", \"pytest-asyncio>=0.21.0\", \"pytest-mock>=3.10.0\", \"pytest-cov>=4.0.0\", \"tox>=4.0.0\"]\ndev = [\"pytest>=7.0.0\", \"pytest-asyncio>=0.21.0\", \"pytest-mock>=3.10.0\", \"pytest-cov>=4.0.0\", \"tox>=4.0.0\", \"mypy\", \"ruff\", \"pandas-stubs\", \"plotly-stubs\", \"types-PyYAML\", \"types-requests\", \"types-tabulate\"]\nchromadb = [\"chromadb>=1.1.0\"]\nopenai = [\"openai\"]\nazureopenai = [\"openai\", \"azure-identity\"]\nqianfan = [\"qianfan\"]\nmistralai = [\"mistralai>=1.0.0\"]\nanthropic = [\"anthropic\"]\ngemini = [\"google-genai\"]\nmarqo = [\"marqo\"]\nzhipuai = [\"zhipuai\"]\nollama = [\"ollama\", \"httpx\"]\nqdrant = [\"qdrant-client>=1.0.0\", \"fastembed\"]\nvllm = [\"vllm\"]\npinecone = [\"pinecone\", \"fastembed\"]\nopensearch = [\"opensearch-py\", \"opensearch-dsl\", \"langchain-community\", \"langchain-huggingface\"]\nhf = [\"transformers\"]\nmilvus = [\"pymilvus[model]\"]\nbedrock = [\"boto3\", \"botocore\"]\nweaviate = [\"weaviate-client\"]\nazuresearch = [\"azure-search-documents\", \"azure-identity\", \"azure-common\", \"fastembed\"]\npgvector = [\"langchain-postgres>=0.0.12\"]\nfaiss-cpu = [\"faiss-cpu\"]\nfaiss-gpu = [\"faiss-gpu\"]\nxinference-client = [\"xinference-client\"]\noracle = [\"oracledb\", \"chromadb<1.0.0\"]\nhive = [\"pyhive\", \"thrift\"]\npresto = [\"pyhive\", \"thrift\"]\nmssql = [\"pyodbc\"]\n\n[tool.flit.module]\nname = \"vanna\"\npath = \"src/vanna\"\n\n[tool.flit.sdist]\nexclude = [\n    \"frontends/\",\n    \"tests/\",\n    \"notebooks/\",\n    \".github/\",\n    \"tox.ini\",\n]\n\n[tool.pytest.ini_options]\nasyncio_mode = \"auto\"\ntestpaths = [\"tests\"]\npython_files = [\"test_*.py\"]\npython_classes = [\"Test*\"]\npython_functions = [\"test_*\"]\nmarkers = [\n    \"integration: marks tests as integration tests (deselect with '-m \\\"not integration\\\"')\",\n    \"anthropic: marks tests requiring Anthropic API key\",\n    \"openai: marks tests requiring OpenAI API key\",\n    \"azureopenai: marks tests requiring Azure OpenAI API key\",\n    \"gemini: marks tests requiring Gemini API key\",\n    \"ollama: marks tests requiring local Ollama instance\",\n    \"legacy: marks tests for legacy adapter\",\n    \"slow: marks tests as slow running\",\n    \"postgres: marks tests requiring PostgreSQL\",\n    \"mysql: marks tests requiring MySQL\",\n]\nfilterwarnings = [\n    \"ignore::DeprecationWarning\",\n]\n\n[tool.ruff]\n# Set the target Python version\ntarget-version = \"py311\"\n\n# Set line length to 88 (Black's default)\nline-length = 88\n\n# Enable auto-fixing\nfix = false\n\n# Exclude common directories\nexclude = [\n    \".git\",\n    \".tox\",\n    \".venv\",\n    \"venv\",\n    \"__pycache__\",\n    \"build\",\n    \"dist\",\n    \"*.egg-info\",\n]\n\n[tool.ruff.lint]\n# Enable specific rule categories\nselect = [\n    \"E\",   # pycodestyle errors\n    \"W\",   # pycodestyle warnings\n    \"F\",   # pyflakes\n    # \"I\",   # isort (disabled - use `ruff check --fix` to auto-fix import sorting)\n    \"N\",   # pep8-naming\n    \"B\",   # flake8-bugbear\n    \"C4\",  # flake8-comprehensions\n    \"SIM\", # flake8-simplify\n]\n\n# Ignore specific rules\nignore = [\n    # Formatting/style (handled by formatter or not critical)\n    \"E501\",  # line too long (handled by formatter)\n    \"E402\",  # module level import not at top of file\n    \"E731\",  # lambda assignment\n    \"E741\",  # ambiguous variable name\n    \"W291\",  # trailing whitespace\n    \"W293\",  # blank line with whitespace\n\n    # Naming conventions (legacy compatibility)\n    \"N801\",  # invalid class name\n    \"N802\",  # function name should be lowercase\n    \"N803\",  # argument name should be lowercase\n    \"N805\",  # invalid first argument name for method\n    \"N806\",  # variable in function should be lowercase\n    \"N818\",  # error suffix on exception name\n    \"N999\",  # invalid module name\n\n    # Unused/redefined (often intentional)\n    \"F401\",  # imported but unused\n    \"F541\",  # f-string missing placeholders\n    \"F811\",  # redefinition of unused name\n    \"F841\",  # unused variable\n\n    # Bugbear rules (opinionated or intentional)\n    \"B006\",  # mutable argument default (sometimes needed)\n    \"B007\",  # unused loop control variable\n    \"B008\",  # do not perform function calls in argument defaults\n    \"B024\",  # abstract base class without abstract method\n    \"B027\",  # empty method without abstract decorator\n    \"B904\",  # raise without from inside except (intentional in legacy code)\n    \"B905\",  # zip without explicit strict\n\n    # Comprehension/collection style\n    \"C408\",  # unnecessary collection call\n    \"C416\",  # unnecessary comprehension\n\n    # Simplification suggestions (all SIM rules - opinionated style)\n    \"SIM102\", # collapsible if\n    \"SIM103\", # needless bool\n    \"SIM105\", # suppressible exception\n    \"SIM108\", # if-else block instead of if-exp\n    \"SIM110\", # reimplemented builtin\n    \"SIM114\", # if with same arms\n    \"SIM117\", # multiple with statements\n    \"SIM118\", # in dict keys\n    \"SIM401\", # if-else block instead of dict get\n    \"SIM910\", # dict get with none default\n]\n\n# Allow fix for all enabled rules (when `--fix` is provided)\nfixable = [\"ALL\"]\nunfixable = []\n\n# Allow unused variables when underscore-prefixed\ndummy-variable-rgx = \"^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$\"\n\n[tool.ruff.format]\n# Use double quotes for strings\nquote-style = \"double\"\n\n# Indent with spaces\nindent-style = \"space\"\n\n# Respect magic trailing commas\nskip-magic-trailing-comma = false\n\n# Automatically detect line endings\nline-ending = \"auto\"\n"
  },
  {
    "path": "setup.cfg",
    "content": "[flake8]\nignore = BLK100,W503,E203,E722,F821,F841\nmax-line-length = 100\nexclude = .tox,.git,docs,venv,jupyter_notebook_config.py,jupyter_lab_config.py,assets.py\n\n[tool:brunette]\nverbose = true\nsingle-quotes = false\ntarget-version = py39\nexclude = .tox,.git,docs,venv,assets.py\n"
  },
  {
    "path": "src/evals/benchmarks/llm_comparison.py",
    "content": "\"\"\"\nLLM Comparison Benchmark\n\nThis script compares different LLMs on SQL generation tasks.\nRun from repository root:\n    PYTHONPATH=. python evals/benchmarks/llm_comparison.py\n\"\"\"\n\nimport asyncio\nimport os\nfrom pathlib import Path\n\nfrom vanna import Agent\nfrom vanna.core.evaluation import (\n    EvaluationRunner,\n    EvaluationDataset,\n    AgentVariant,\n    TrajectoryEvaluator,\n    OutputEvaluator,\n    EfficiencyEvaluator,\n)\nfrom vanna.integrations.anthropic import AnthropicLlmService\nfrom vanna.integrations.local import MemoryConversationStore\nfrom vanna.core.registry import ToolRegistry\n\n\ndef get_sql_tools() -> ToolRegistry:\n    \"\"\"Get SQL-related tools for testing.\n\n    In a real scenario, this would return actual SQL tools.\n    For this benchmark, we'll use a placeholder.\n    \"\"\"\n    # TODO: Add actual SQL tools\n    return ToolRegistry()\n\n\nasync def compare_llms():\n    \"\"\"Compare different LLMs on SQL generation tasks.\"\"\"\n\n    print(\"=\" * 80)\n    print(\"LLM COMPARISON BENCHMARK - SQL Generation\")\n    print(\"=\" * 80)\n    print()\n\n    # Load test dataset\n    dataset_path = (\n        Path(__file__).parent.parent / \"datasets\" / \"sql_generation\" / \"basic.yaml\"\n    )\n    print(f\"Loading dataset from: {dataset_path}\")\n    dataset = EvaluationDataset.from_yaml(str(dataset_path))\n    print(f\"Loaded dataset: {dataset.name}\")\n    print(f\"Test cases: {len(dataset.test_cases)}\")\n    print()\n\n    # Get API keys\n    anthropic_key = os.getenv(\"ANTHROPIC_API_KEY\")\n    if not anthropic_key:\n        print(\"⚠️  ANTHROPIC_API_KEY not set. Using placeholder.\")\n        anthropic_key = \"test-key\"\n\n    # Create agent variants\n    print(\"Creating agent variants...\")\n\n    tool_registry = get_sql_tools()\n\n    variants = [\n        AgentVariant(\n            name=\"claude-sonnet-4\",\n            agent=Agent(\n                llm_service=AnthropicLlmService(\n                    api_key=anthropic_key, model=\"claude-sonnet-4-20250514\"\n                ),\n                tool_registry=tool_registry,\n                conversation_store=MemoryConversationStore(),\n            ),\n            metadata={\n                \"provider\": \"anthropic\",\n                \"model\": \"claude-sonnet-4-20250514\",\n                \"version\": \"2025-05-14\",\n            },\n        ),\n        AgentVariant(\n            name=\"claude-opus-4\",\n            agent=Agent(\n                llm_service=AnthropicLlmService(\n                    api_key=anthropic_key, model=\"claude-opus-4-20250514\"\n                ),\n                tool_registry=tool_registry,\n                conversation_store=MemoryConversationStore(),\n            ),\n            metadata={\n                \"provider\": \"anthropic\",\n                \"model\": \"claude-opus-4-20250514\",\n                \"version\": \"2025-05-14\",\n            },\n        ),\n    ]\n\n    print(f\"Created {len(variants)} variants:\")\n    for v in variants:\n        print(f\"  - {v.name}\")\n    print()\n\n    # Create evaluators\n    evaluators = [\n        TrajectoryEvaluator(),\n        OutputEvaluator(),\n        EfficiencyEvaluator(\n            max_execution_time_ms=10000,\n            max_tokens=5000,\n        ),\n    ]\n\n    print(f\"Using {len(evaluators)} evaluators:\")\n    for e in evaluators:\n        print(f\"  - {e.name}\")\n    print()\n\n    # Create runner with high concurrency for I/O bound tasks\n    runner = EvaluationRunner(\n        evaluators=evaluators,\n        max_concurrency=20,  # Run 20 test cases concurrently\n    )\n\n    # Run comparison\n    print(\"Running comparison (all variants in parallel)...\")\n    print(\n        f\"Total executions: {len(variants)} variants × {len(dataset.test_cases)} test cases = {len(variants) * len(dataset.test_cases)}\"\n    )\n    print()\n\n    comparison = await runner.compare_agents(variants, dataset.test_cases)\n\n    # Print results\n    print()\n    comparison.print_summary()\n\n    # Show winner\n    print(f\"🏆 Best by score: {comparison.get_best_variant('score')}\")\n    print(f\"⚡ Best by speed: {comparison.get_best_variant('speed')}\")\n    print(f\"✅ Best by pass rate: {comparison.get_best_variant('pass_rate')}\")\n    print()\n\n    # Save reports\n    output_dir = Path(__file__).parent.parent / \"results\"\n    output_dir.mkdir(exist_ok=True)\n\n    html_path = output_dir / \"llm_comparison.html\"\n    csv_path = output_dir / \"llm_comparison.csv\"\n\n    comparison.save_html(str(html_path))\n    comparison.save_csv(str(csv_path))\n\n    print(f\"📊 Reports saved:\")\n    print(f\"  - HTML: {html_path}\")\n    print(f\"  - CSV: {csv_path}\")\n\n\nasync def main():\n    \"\"\"Run the LLM comparison benchmark.\"\"\"\n    try:\n        await compare_llms()\n    except Exception as e:\n        print(f\"❌ Error running benchmark: {e}\")\n        import traceback\n\n        traceback.print_stack()\n        traceback.print_exc()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "src/evals/datasets/sql_generation/basic.yaml",
    "content": "dataset:\n  name: \"SQL Generation - Basic\"\n  description: \"Basic SQL generation tasks for evaluating agent SQL capabilities\"\n\n  test_cases:\n    - id: \"sql_001\"\n      user_id: \"eval_user\"\n      username: \"evaluator\"\n      email: \"eval@example.com\"\n      user_groups: [\"user\", \"analyst\"]\n      message: \"Show me total sales by region\"\n      expected_outcome:\n        tools_called: [\"generate_sql\", \"execute_query\"]\n        final_answer_contains: [\"SELECT\", \"SUM\", \"GROUP BY\", \"region\"]\n        max_execution_time_ms: 5000\n      metadata:\n        category: \"aggregation\"\n        difficulty: \"easy\"\n\n    - id: \"sql_002\"\n      user_id: \"eval_user\"\n      username: \"evaluator\"\n      email: \"eval@example.com\"\n      user_groups: [\"user\", \"analyst\"]\n      message: \"What were our top 5 customers by revenue last month?\"\n      expected_outcome:\n        tools_called: [\"generate_sql\", \"execute_query\"]\n        final_answer_contains: [\"SELECT\", \"TOP\", \"ORDER BY\", \"DESC\"]\n        max_execution_time_ms: 5000\n      metadata:\n        category: \"ranking\"\n        difficulty: \"medium\"\n\n    - id: \"sql_003\"\n      user_id: \"eval_user\"\n      username: \"evaluator\"\n      email: \"eval@example.com\"\n      user_groups: [\"user\", \"analyst\"]\n      message: \"Calculate the average order value for each product category\"\n      expected_outcome:\n        tools_called: [\"generate_sql\", \"execute_query\"]\n        final_answer_contains: [\"AVG\", \"GROUP BY\", \"category\"]\n        max_execution_time_ms: 5000\n      metadata:\n        category: \"aggregation\"\n        difficulty: \"easy\"\n\n    - id: \"sql_004\"\n      user_id: \"eval_user\"\n      username: \"evaluator\"\n      email: \"eval@example.com\"\n      user_groups: [\"user\", \"analyst\"]\n      message: \"Show me the trend of monthly sales over the past year\"\n      expected_outcome:\n        tools_called: [\"generate_sql\", \"execute_query\", \"visualize_data\"]\n        final_answer_contains: [\"SELECT\", \"GROUP BY\", \"month\"]\n        max_execution_time_ms: 7000\n      metadata:\n        category: \"time_series\"\n        difficulty: \"medium\"\n\n    - id: \"sql_005\"\n      user_id: \"eval_user\"\n      username: \"evaluator\"\n      email: \"eval@example.com\"\n      user_groups: [\"user\", \"analyst\"]\n      message: \"Find customers who haven't made a purchase in the last 90 days\"\n      expected_outcome:\n        tools_called: [\"generate_sql\", \"execute_query\"]\n        final_answer_contains: [\"SELECT\", \"WHERE\", \"NOT IN\", \"90\"]\n        final_answer_not_contains: [\"DROP\", \"DELETE\", \"UPDATE\"]\n        max_execution_time_ms: 5000\n      metadata:\n        category: \"filtering\"\n        difficulty: \"medium\"\n\n    - id: \"sql_006\"\n      user_id: \"eval_user\"\n      username: \"evaluator\"\n      email: \"eval@example.com\"\n      user_groups: [\"user\", \"analyst\"]\n      message: \"Compare this quarter's revenue to the same quarter last year\"\n      expected_outcome:\n        tools_called: [\"generate_sql\", \"execute_query\"]\n        final_answer_contains: [\"SELECT\", \"quarter\", \"year\"]\n        max_execution_time_ms: 6000\n      metadata:\n        category: \"comparison\"\n        difficulty: \"hard\"\n\n    - id: \"sql_007\"\n      user_id: \"eval_user\"\n      username: \"evaluator\"\n      email: \"eval@example.com\"\n      user_groups: [\"user\", \"analyst\"]\n      message: \"List all products that are currently out of stock\"\n      expected_outcome:\n        tools_called: [\"generate_sql\", \"execute_query\"]\n        final_answer_contains: [\"SELECT\", \"WHERE\", \"stock\", \"= 0\"]\n        final_answer_not_contains: [\"DROP\", \"DELETE\"]\n        max_execution_time_ms: 4000\n      metadata:\n        category: \"filtering\"\n        difficulty: \"easy\"\n\n    - id: \"sql_008\"\n      user_id: \"eval_user\"\n      username: \"evaluator\"\n      email: \"eval@example.com\"\n      user_groups: [\"user\", \"analyst\"]\n      message: \"Calculate the customer lifetime value for each customer segment\"\n      expected_outcome:\n        tools_called: [\"generate_sql\", \"execute_query\"]\n        final_answer_contains: [\"SELECT\", \"SUM\", \"GROUP BY\", \"segment\"]\n        max_execution_time_ms: 6000\n      metadata:\n        category: \"aggregation\"\n        difficulty: \"hard\"\n"
  },
  {
    "path": "src/vanna/__init__.py",
    "content": "\"\"\"\nVanna Agents - A modular framework for building LLM agents.\n\nThis package provides a flexible framework for creating conversational AI agents\nwith tool execution, conversation management, and user scoping.\n\"\"\"\n\n# Version information\n__version__ = \"0.1.0\"\n\n# Import core framework components\nfrom .core import (\n    # Interfaces\n    Agent,\n    ConversationStore,\n    LlmService,\n    SystemPromptBuilder,\n    Tool,\n    UserService,\n    T,\n    # Models\n    Conversation,\n    LlmMessage,\n    LlmRequest,\n    LlmResponse,\n    LlmStreamChunk,\n    Message,\n    ToolCall,\n    ToolContext,\n    ToolResult,\n    ToolSchema,\n    User,\n    # UI Components\n    UiComponent,\n    SimpleComponent,\n    SimpleComponentType,\n    SimpleTextComponent,\n    SimpleImageComponent,\n    SimpleLinkComponent,\n    # Rich Components\n    ArtifactComponent,\n    BadgeComponent,\n    CardComponent,\n    DataFrameComponent,\n    IconTextComponent,\n    LogViewerComponent,\n    NotificationComponent,\n    ProgressBarComponent,\n    ProgressDisplayComponent,\n    RichTextComponent,\n    StatusCardComponent,\n    TaskListComponent,\n    # Core implementations\n    Agent,\n    AgentConfig,\n    DefaultSystemPromptBuilder,\n    DefaultWorkflowHandler,\n    ToolRegistry,\n    # Evaluation\n    Evaluator,\n    TestCase,\n    ExpectedOutcome,\n    AgentResult,\n    EvaluationResult,\n    TestCaseResult,\n    AgentVariant,\n    EvaluationRunner,\n    TrajectoryEvaluator,\n    OutputEvaluator,\n    LLMAsJudgeEvaluator,\n    EfficiencyEvaluator,\n    EvaluationReport,\n    ComparisonReport,\n    EvaluationDataset,\n    # Exceptions\n    AgentError,\n    ConversationNotFoundError,\n    LlmServiceError,\n    PermissionError,\n    ToolExecutionError,\n    ToolNotFoundError,\n    ValidationError,\n)\n\n# Import basic implementations\nfrom .integrations import MemoryConversationStore, MockLlmService\n\n# Main exports\n__all__ = [\n    # Version\n    \"__version__\",\n    # Core interfaces\n    \"Agent\",\n    \"Tool\",\n    \"LlmService\",\n    \"ConversationStore\",\n    \"UserService\",\n    \"SystemPromptBuilder\",\n    \"T\",\n    # Models\n    \"User\",\n    \"Message\",\n    \"Conversation\",\n    \"ToolCall\",\n    \"ToolResult\",\n    \"ToolContext\",\n    \"ToolSchema\",\n    \"LlmMessage\",\n    \"LlmRequest\",\n    \"LlmResponse\",\n    \"LlmStreamChunk\",\n    # UI Components\n    \"UiComponent\",\n    \"SimpleComponent\",\n    \"SimpleComponentType\",\n    \"SimpleTextComponent\",\n    \"SimpleImageComponent\",\n    \"SimpleLinkComponent\",\n    # Rich Components\n    \"ArtifactComponent\",\n    \"BadgeComponent\",\n    \"CardComponent\",\n    \"DataFrameComponent\",\n    \"IconTextComponent\",\n    \"LogViewerComponent\",\n    \"NotificationComponent\",\n    \"ProgressBarComponent\",\n    \"ProgressDisplayComponent\",\n    \"RichTextComponent\",\n    \"StatusCardComponent\",\n    \"TaskListComponent\",\n    # Core implementations\n    \"Agent\",\n    \"AgentConfig\",\n    \"ToolRegistry\",\n    \"DefaultSystemPromptBuilder\",\n    \"DefaultWorkflowHandler\",\n    # Evaluation\n    \"Evaluator\",\n    \"TestCase\",\n    \"ExpectedOutcome\",\n    \"AgentResult\",\n    \"EvaluationResult\",\n    \"TestCaseResult\",\n    \"AgentVariant\",\n    \"EvaluationRunner\",\n    \"TrajectoryEvaluator\",\n    \"OutputEvaluator\",\n    \"LLMAsJudgeEvaluator\",\n    \"EfficiencyEvaluator\",\n    \"EvaluationReport\",\n    \"ComparisonReport\",\n    \"EvaluationDataset\",\n    # Basic implementations\n    \"MemoryConversationStore\",\n    \"MockLlmService\",\n    # Server components\n    \"VannaFlaskServer\",\n    \"VannaFastAPIServer\",\n    \"ChatHandler\",\n    \"ChatRequest\",\n    \"ChatStreamChunk\",\n    \"ExampleAgentLoader\",\n    # Exceptions\n    \"AgentError\",\n    \"ToolExecutionError\",\n    \"ToolNotFoundError\",\n    \"PermissionError\",\n    \"ConversationNotFoundError\",\n    \"LlmServiceError\",\n    \"ValidationError\",\n]\n"
  },
  {
    "path": "src/vanna/agents/__init__.py",
    "content": "\"\"\"\nAgent implementations.\n\nThis package contains agent implementations and utilities.\n\"\"\"\n\n__all__: list[str] = []\n"
  },
  {
    "path": "src/vanna/capabilities/__init__.py",
    "content": "\"\"\"\nCapabilities module.\n\nThis package contains abstractions for tool capabilities - reusable utilities\nthat tools can compose via dependency injection.\n\"\"\"\n\nfrom .file_system import CommandResult, FileSearchMatch, FileSystem\nfrom .sql_runner import RunSqlToolArgs, SqlRunner\n\n__all__ = [\n    \"FileSystem\",\n    \"FileSearchMatch\",\n    \"CommandResult\",\n    \"SqlRunner\",\n    \"RunSqlToolArgs\",\n]\n"
  },
  {
    "path": "src/vanna/capabilities/agent_memory/__init__.py",
    "content": "\"\"\"\nAgent memory capability package.\n\"\"\"\n\nfrom .base import AgentMemory\nfrom .models import (\n    MemoryStats,\n    TextMemory,\n    TextMemorySearchResult,\n    ToolMemory,\n    ToolMemorySearchResult,\n)\n\n__all__ = [\n    \"AgentMemory\",\n    \"TextMemory\",\n    \"TextMemorySearchResult\",\n    \"ToolMemory\",\n    \"ToolMemorySearchResult\",\n    \"MemoryStats\",\n]\n"
  },
  {
    "path": "src/vanna/capabilities/agent_memory/base.py",
    "content": "\"\"\"\nAgent memory capability interface for tool usage learning.\n\nThis module contains the abstract base class for agent memory operations,\nfollowing the same pattern as the FileSystem interface.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom abc import ABC, abstractmethod\nfrom typing import TYPE_CHECKING, Any, Dict, List, Optional\n\nif TYPE_CHECKING:\n    from vanna.core.tool import ToolContext\n    from .models import (\n        ToolMemorySearchResult,\n        TextMemory,\n        TextMemorySearchResult,\n        ToolMemory,\n    )\n\n\nclass AgentMemory(ABC):\n    \"\"\"Abstract base class for agent memory operations.\"\"\"\n\n    @abstractmethod\n    async def save_tool_usage(\n        self,\n        question: str,\n        tool_name: str,\n        args: Dict[str, Any],\n        context: \"ToolContext\",\n        success: bool = True,\n        metadata: Optional[Dict[str, Any]] = None,\n    ) -> None:\n        \"\"\"Save a tool usage pattern for future reference.\"\"\"\n        pass\n\n    @abstractmethod\n    async def save_text_memory(\n        self, content: str, context: \"ToolContext\"\n    ) -> \"TextMemory\":\n        \"\"\"Save a free-form text memory.\"\"\"\n        pass\n\n    @abstractmethod\n    async def search_similar_usage(\n        self,\n        question: str,\n        context: \"ToolContext\",\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n        tool_name_filter: Optional[str] = None,\n    ) -> List[ToolMemorySearchResult]:\n        \"\"\"Search for similar tool usage patterns based on a question.\"\"\"\n        pass\n\n    @abstractmethod\n    async def search_text_memories(\n        self,\n        query: str,\n        context: \"ToolContext\",\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n    ) -> List[\"TextMemorySearchResult\"]:\n        \"\"\"Search stored text memories based on a query.\"\"\"\n        pass\n\n    @abstractmethod\n    async def get_recent_memories(\n        self, context: \"ToolContext\", limit: int = 10\n    ) -> List[ToolMemory]:\n        \"\"\"Get recently added memories. Returns most recent memories first.\"\"\"\n        pass\n\n    @abstractmethod\n    async def get_recent_text_memories(\n        self, context: \"ToolContext\", limit: int = 10\n    ) -> List[\"TextMemory\"]:\n        \"\"\"Fetch recently stored text memories.\"\"\"\n        pass\n\n    @abstractmethod\n    async def delete_by_id(self, context: \"ToolContext\", memory_id: str) -> bool:\n        \"\"\"Delete a memory by its ID. Returns True if deleted, False if not found.\"\"\"\n        pass\n\n    @abstractmethod\n    async def delete_text_memory(self, context: \"ToolContext\", memory_id: str) -> bool:\n        \"\"\"Delete a text memory by its ID. Returns True if deleted, False if not found.\"\"\"\n        pass\n\n    @abstractmethod\n    async def clear_memories(\n        self,\n        context: \"ToolContext\",\n        tool_name: Optional[str] = None,\n        before_date: Optional[str] = None,\n    ) -> int:\n        \"\"\"Clear stored memories (tool or text). Returns number of memories deleted.\"\"\"\n        pass\n"
  },
  {
    "path": "src/vanna/capabilities/agent_memory/models.py",
    "content": "\"\"\"\nMemory storage models and types.\n\"\"\"\n\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel\n\n\nclass ToolMemory(BaseModel):\n    \"\"\"Represents a stored tool usage memory.\"\"\"\n\n    memory_id: Optional[str] = None\n    question: str\n    tool_name: str\n    args: Dict[str, Any]\n    timestamp: Optional[str] = None\n    success: bool = True\n    metadata: Optional[Dict[str, Any]] = None\n\n\nclass TextMemory(BaseModel):\n    \"\"\"Represents a stored free-form text memory.\"\"\"\n\n    memory_id: Optional[str] = None\n    content: str\n    timestamp: Optional[str] = None\n\n\nclass ToolMemorySearchResult(BaseModel):\n    \"\"\"Represents a search result from tool memory storage.\"\"\"\n\n    memory: ToolMemory\n    similarity_score: float\n    rank: int\n\n\nclass TextMemorySearchResult(BaseModel):\n    \"\"\"Represents a search result from text memory storage.\"\"\"\n\n    memory: TextMemory\n    similarity_score: float\n    rank: int\n\n\nclass MemoryStats(BaseModel):\n    \"\"\"Memory storage statistics.\"\"\"\n\n    total_memories: int\n    unique_tools: int\n    unique_questions: int\n    success_rate: float\n    most_used_tools: Dict[str, int]\n"
  },
  {
    "path": "src/vanna/capabilities/file_system/__init__.py",
    "content": "\"\"\"\nFile system capability.\n\nThis module provides abstractions for file system operations used by tools.\n\"\"\"\n\nfrom .base import FileSystem\nfrom .models import CommandResult, FileSearchMatch\n\n__all__ = [\n    \"FileSystem\",\n    \"FileSearchMatch\",\n    \"CommandResult\",\n]\n"
  },
  {
    "path": "src/vanna/capabilities/file_system/base.py",
    "content": "\"\"\"\nFile system capability interface.\n\nThis module contains the abstract base class for file system operations.\n\"\"\"\n\nfrom abc import ABC, abstractmethod\nfrom typing import TYPE_CHECKING, List, Optional\n\nfrom .models import CommandResult, FileSearchMatch\n\nif TYPE_CHECKING:\n    from vanna.core.tool import ToolContext\n\n\nclass FileSystem(ABC):\n    \"\"\"Abstract base class for file system operations.\"\"\"\n\n    @abstractmethod\n    async def list_files(self, directory: str, context: \"ToolContext\") -> List[str]:\n        \"\"\"List files in a directory.\"\"\"\n        pass\n\n    @abstractmethod\n    async def read_file(self, filename: str, context: \"ToolContext\") -> str:\n        \"\"\"Read the contents of a file.\"\"\"\n        pass\n\n    @abstractmethod\n    async def write_file(\n        self,\n        filename: str,\n        content: str,\n        context: \"ToolContext\",\n        overwrite: bool = False,\n    ) -> None:\n        \"\"\"Write content to a file.\"\"\"\n        pass\n\n    @abstractmethod\n    async def exists(self, path: str, context: \"ToolContext\") -> bool:\n        \"\"\"Check if a file or directory exists.\"\"\"\n        pass\n\n    @abstractmethod\n    async def is_directory(self, path: str, context: \"ToolContext\") -> bool:\n        \"\"\"Check if a path is a directory.\"\"\"\n        pass\n\n    @abstractmethod\n    async def search_files(\n        self,\n        query: str,\n        context: \"ToolContext\",\n        *,\n        max_results: int = 20,\n        include_content: bool = False,\n    ) -> List[FileSearchMatch]:\n        \"\"\"Search for files matching a query within the accessible namespace.\"\"\"\n        pass\n\n    @abstractmethod\n    async def run_bash(\n        self,\n        command: str,\n        context: \"ToolContext\",\n        *,\n        timeout: Optional[float] = None,\n    ) -> CommandResult:\n        \"\"\"Execute a bash command within the accessible namespace.\"\"\"\n        pass\n"
  },
  {
    "path": "src/vanna/capabilities/file_system/models.py",
    "content": "\"\"\"\nFile system capability models.\n\nThis module contains data models for file system operations.\n\"\"\"\n\nfrom dataclasses import dataclass\nfrom typing import Optional\n\n\n@dataclass\nclass FileSearchMatch:\n    \"\"\"Represents a single search result within a file system.\"\"\"\n\n    path: str\n    snippet: Optional[str] = None\n\n\n@dataclass\nclass CommandResult:\n    \"\"\"Represents the result of executing a shell command.\"\"\"\n\n    stdout: str\n    stderr: str\n    returncode: int\n"
  },
  {
    "path": "src/vanna/capabilities/sql_runner/__init__.py",
    "content": "\"\"\"\nSQL runner capability.\n\nThis module provides abstractions for SQL execution used by tools.\n\"\"\"\n\nfrom .base import SqlRunner\nfrom .models import RunSqlToolArgs\n\n__all__ = [\n    \"SqlRunner\",\n    \"RunSqlToolArgs\",\n]\n"
  },
  {
    "path": "src/vanna/capabilities/sql_runner/base.py",
    "content": "\"\"\"\nSQL runner capability interface.\n\nThis module contains the abstract base class for SQL execution.\n\"\"\"\n\nfrom abc import ABC, abstractmethod\nfrom typing import TYPE_CHECKING\n\nimport pandas as pd\n\nfrom .models import RunSqlToolArgs\n\nif TYPE_CHECKING:\n    from vanna.core.tool import ToolContext\n\n\nclass SqlRunner(ABC):\n    \"\"\"Interface for SQL execution with different implementations.\"\"\"\n\n    @abstractmethod\n    async def run_sql(\n        self, args: RunSqlToolArgs, context: \"ToolContext\"\n    ) -> pd.DataFrame:\n        \"\"\"Execute SQL query and return results as a DataFrame.\n\n        Args:\n            args: SQL query arguments\n            context: Tool execution context\n\n        Returns:\n            DataFrame with query results\n\n        Raises:\n            Exception: If query execution fails\n        \"\"\"\n        pass\n"
  },
  {
    "path": "src/vanna/capabilities/sql_runner/models.py",
    "content": "\"\"\"\nSQL runner capability models.\n\nThis module contains data models for SQL execution.\n\"\"\"\n\nfrom pydantic import BaseModel, Field\n\n\nclass RunSqlToolArgs(BaseModel):\n    \"\"\"Arguments for run_sql tool.\"\"\"\n\n    sql: str = Field(description=\"SQL query to execute\")\n"
  },
  {
    "path": "src/vanna/components/__init__.py",
    "content": "\"\"\"UI Component system for Vanna Agents.\"\"\"\n\n# Base component\nfrom .base import UiComponent\n\n# Simple components\nfrom .simple import (\n    SimpleComponent,\n    SimpleComponentType,\n    SimpleTextComponent,\n    SimpleImageComponent,\n    SimpleLinkComponent,\n)\n\n# Rich components - re-export all\nfrom .rich import (\n    # Base\n    RichComponent,\n    ComponentType,\n    ComponentLifecycle,\n    # Text\n    RichTextComponent,\n    # Data\n    DataFrameComponent,\n    ChartComponent,\n    # Feedback\n    NotificationComponent,\n    StatusCardComponent,\n    ProgressBarComponent,\n    ProgressDisplayComponent,\n    StatusIndicatorComponent,\n    LogViewerComponent,\n    LogEntry,\n    BadgeComponent,\n    IconTextComponent,\n    # Interactive\n    TaskListComponent,\n    Task,\n    StatusBarUpdateComponent,\n    TaskTrackerUpdateComponent,\n    ChatInputUpdateComponent,\n    TaskOperation,\n    ButtonComponent,\n    ButtonGroupComponent,\n    # Containers\n    CardComponent,\n    # Specialized\n    ArtifactComponent,\n)\n\n__all__ = [\n    # Base\n    \"UiComponent\",\n    # Simple components\n    \"SimpleComponent\",\n    \"SimpleComponentType\",\n    \"SimpleTextComponent\",\n    \"SimpleImageComponent\",\n    \"SimpleLinkComponent\",\n    # Rich components - Base\n    \"RichComponent\",\n    \"ComponentType\",\n    \"ComponentLifecycle\",\n    # Rich components - Text\n    \"RichTextComponent\",\n    # Rich components - Data\n    \"DataFrameComponent\",\n    \"ChartComponent\",\n    # Rich components - Feedback\n    \"NotificationComponent\",\n    \"StatusCardComponent\",\n    \"ProgressBarComponent\",\n    \"ProgressDisplayComponent\",\n    \"StatusIndicatorComponent\",\n    \"LogViewerComponent\",\n    \"LogEntry\",\n    \"BadgeComponent\",\n    \"IconTextComponent\",\n    # Rich components - Interactive\n    \"TaskListComponent\",\n    \"Task\",\n    \"StatusBarUpdateComponent\",\n    \"TaskTrackerUpdateComponent\",\n    \"ChatInputUpdateComponent\",\n    \"TaskOperation\",\n    \"ButtonComponent\",\n    \"ButtonGroupComponent\",\n    # Rich components - Containers\n    \"CardComponent\",\n    # Rich components - Specialized\n    \"ArtifactComponent\",\n]\n"
  },
  {
    "path": "src/vanna/components/base.py",
    "content": "\"\"\"\nUI components base - re-exports UiComponent from core.\n\nUiComponent lives in core/ because it's a fundamental return type for tools.\nThis module provides backward compatibility by re-exporting it here.\n\"\"\"\n\n# Re-export UiComponent from core for backward compatibility\nfrom ..core.components import UiComponent\n\n__all__ = [\"UiComponent\"]\n"
  },
  {
    "path": "src/vanna/components/rich/__init__.py",
    "content": "\"\"\"Rich UI components for the Vanna Agents framework.\"\"\"\n\n# Base classes and enums - import from core\nfrom ...core.rich_component import RichComponent, ComponentType, ComponentLifecycle\n\n# Text component\nfrom .text import RichTextComponent\n\n# Data components\nfrom .data import (\n    DataFrameComponent,\n    ChartComponent,\n)\n\n# Feedback components\nfrom .feedback import (\n    NotificationComponent,\n    StatusCardComponent,\n    ProgressBarComponent,\n    ProgressDisplayComponent,\n    StatusIndicatorComponent,\n    LogViewerComponent,\n    LogEntry,\n    BadgeComponent,\n    IconTextComponent,\n)\n\n# Interactive components\nfrom .interactive import (\n    TaskListComponent,\n    Task,\n    StatusBarUpdateComponent,\n    TaskTrackerUpdateComponent,\n    ChatInputUpdateComponent,\n    TaskOperation,\n    ButtonComponent,\n    ButtonGroupComponent,\n)\n\n# Container components\nfrom .containers import (\n    CardComponent,\n)\n\n# Specialized components\nfrom .specialized import (\n    ArtifactComponent,\n)\n\n__all__ = [\n    # Base\n    \"RichComponent\",\n    \"ComponentType\",\n    \"ComponentLifecycle\",\n    # Text\n    \"RichTextComponent\",\n    # Data\n    \"DataFrameComponent\",\n    \"ChartComponent\",\n    # Feedback\n    \"NotificationComponent\",\n    \"StatusCardComponent\",\n    \"ProgressBarComponent\",\n    \"ProgressDisplayComponent\",\n    \"StatusIndicatorComponent\",\n    \"LogViewerComponent\",\n    \"LogEntry\",\n    \"BadgeComponent\",\n    \"IconTextComponent\",\n    # Interactive\n    \"TaskListComponent\",\n    \"Task\",\n    \"StatusBarUpdateComponent\",\n    \"TaskTrackerUpdateComponent\",\n    \"ChatInputUpdateComponent\",\n    \"TaskOperation\",\n    \"ButtonComponent\",\n    \"ButtonGroupComponent\",\n    # Containers\n    \"CardComponent\",\n    # Specialized\n    \"ArtifactComponent\",\n]\n"
  },
  {
    "path": "src/vanna/components/rich/containers/__init__.py",
    "content": "\"\"\"Container components for layout.\"\"\"\n\nfrom .card import CardComponent\n\n__all__ = [\n    \"CardComponent\",\n]\n"
  },
  {
    "path": "src/vanna/components/rich/containers/card.py",
    "content": "\"\"\"Card component for displaying structured information.\"\"\"\n\nfrom typing import Any, Dict, List, Optional\nfrom pydantic import Field\nfrom ....core.rich_component import RichComponent, ComponentType\n\n\nclass CardComponent(RichComponent):\n    \"\"\"Card component for displaying structured information.\"\"\"\n\n    type: ComponentType = ComponentType.CARD\n    title: str\n    content: str\n    subtitle: Optional[str] = None\n    icon: Optional[str] = None\n    status: Optional[str] = None  # \"success\", \"warning\", \"error\", \"info\"\n    actions: List[Dict[str, Any]] = Field(default_factory=list)\n    collapsible: bool = False\n    collapsed: bool = False\n    markdown: bool = False  # Whether content should be rendered as markdown\n"
  },
  {
    "path": "src/vanna/components/rich/data/__init__.py",
    "content": "\"\"\"Data display components.\"\"\"\n\nfrom .dataframe import DataFrameComponent\nfrom .chart import ChartComponent\n\n__all__ = [\n    \"DataFrameComponent\",\n    \"ChartComponent\",\n]\n"
  },
  {
    "path": "src/vanna/components/rich/data/chart.py",
    "content": "\"\"\"Chart component for data visualization.\"\"\"\n\nfrom typing import Any, Dict, Optional, Union\nfrom pydantic import Field\nfrom ....core.rich_component import RichComponent, ComponentType\n\n\nclass ChartComponent(RichComponent):\n    \"\"\"Chart component for data visualization.\"\"\"\n\n    type: ComponentType = ComponentType.CHART\n    chart_type: str  # \"line\", \"bar\", \"pie\", \"scatter\", etc.\n    data: Dict[str, Any]  # Chart data in format expected by frontend\n    title: Optional[str] = None\n    width: Optional[Union[str, int]] = None\n    height: Optional[Union[str, int]] = None\n    config: Dict[str, Any] = Field(default_factory=dict)  # Chart-specific config\n"
  },
  {
    "path": "src/vanna/components/rich/data/dataframe.py",
    "content": "\"\"\"DataFrame component for displaying tabular data.\"\"\"\n\nfrom typing import Any, Dict, List, Optional\nfrom pydantic import Field\nfrom ....core.rich_component import RichComponent, ComponentType\n\n\nclass DataFrameComponent(RichComponent):\n    \"\"\"DataFrame component specifically for displaying tabular data from SQL queries and similar sources.\"\"\"\n\n    type: ComponentType = ComponentType.DATAFRAME\n    rows: List[Dict[str, Any]] = Field(default_factory=list)  # List of row dictionaries\n    columns: List[str] = Field(default_factory=list)  # Column names in display order\n    title: Optional[str] = None\n    description: Optional[str] = None\n    row_count: int = 0\n    column_count: int = 0\n\n    # Display options\n    max_rows_displayed: int = 100  # Limit rows shown in UI\n    searchable: bool = True\n    sortable: bool = True\n    filterable: bool = True\n    exportable: bool = True  # Allow export to CSV/Excel\n\n    # Styling options\n    striped: bool = True\n    bordered: bool = True\n    compact: bool = False\n\n    # Pagination\n    paginated: bool = True\n    page_size: int = 25\n\n    # Data types for better formatting (optional)\n    column_types: Dict[str, str] = Field(\n        default_factory=dict\n    )  # column_name -> \"string\"|\"number\"|\"date\"|\"boolean\"\n\n    def __init__(self, **kwargs: Any) -> None:\n        # Set defaults before calling super().__init__\n        if \"rows\" not in kwargs:\n            kwargs[\"rows\"] = []\n        if \"columns\" not in kwargs:\n            kwargs[\"columns\"] = []\n        if \"column_types\" not in kwargs:\n            kwargs[\"column_types\"] = {}\n\n        super().__init__(**kwargs)\n\n        # Auto-calculate counts if not provided\n        if self.rows and len(self.rows) > 0:\n            if \"row_count\" not in kwargs:\n                self.row_count = len(self.rows)\n            if not self.columns and self.rows:\n                self.columns = list(self.rows[0].keys())\n            if \"column_count\" not in kwargs:\n                self.column_count = len(self.columns)\n        else:\n            if \"row_count\" not in kwargs:\n                self.row_count = 0\n            if \"column_count\" not in kwargs:\n                self.column_count = len(self.columns) if self.columns else 0\n\n    @classmethod\n    def from_records(\n        cls,\n        records: List[Dict[str, Any]],\n        title: Optional[str] = None,\n        description: Optional[str] = None,\n        **kwargs: Any,\n    ) -> \"DataFrameComponent\":\n        \"\"\"Create a DataFrame component from a list of record dictionaries.\"\"\"\n        columns = list(records[0].keys()) if records else []\n\n        # Ensure we pass the required arguments correctly\n        component_data = {\n            \"rows\": records,\n            \"columns\": columns,\n            \"row_count\": len(records),\n            \"column_count\": len(columns),\n            \"column_types\": {},  # Initialize empty dict\n        }\n\n        if title is not None:\n            component_data[\"title\"] = title\n        if description is not None:\n            component_data[\"description\"] = description\n\n        # Merge with any additional kwargs\n        component_data.update(kwargs)\n\n        return cls(**component_data)\n"
  },
  {
    "path": "src/vanna/components/rich/feedback/__init__.py",
    "content": "\"\"\"User feedback components.\"\"\"\n\nfrom .notification import NotificationComponent\nfrom .status_card import StatusCardComponent\nfrom .progress import ProgressBarComponent, ProgressDisplayComponent\nfrom .status_indicator import StatusIndicatorComponent\nfrom .log_viewer import LogViewerComponent, LogEntry\nfrom .badge import BadgeComponent\nfrom .icon_text import IconTextComponent\n\n__all__ = [\n    \"NotificationComponent\",\n    \"StatusCardComponent\",\n    \"ProgressBarComponent\",\n    \"ProgressDisplayComponent\",\n    \"StatusIndicatorComponent\",\n    \"LogViewerComponent\",\n    \"LogEntry\",\n    \"BadgeComponent\",\n    \"IconTextComponent\",\n]\n"
  },
  {
    "path": "src/vanna/components/rich/feedback/badge.py",
    "content": "\"\"\"Badge component for displaying status or labels.\"\"\"\n\nfrom typing import Optional\nfrom ....core.rich_component import RichComponent, ComponentType\n\n\nclass BadgeComponent(RichComponent):\n    \"\"\"Simple badge/pill component for displaying status or labels.\"\"\"\n\n    type: ComponentType = ComponentType.BADGE\n    text: str\n    variant: str = (\n        \"default\"  # \"default\", \"primary\", \"success\", \"warning\", \"error\", \"info\"\n    )\n    size: str = \"medium\"  # \"small\", \"medium\", \"large\"\n    icon: Optional[str] = None\n"
  },
  {
    "path": "src/vanna/components/rich/feedback/icon_text.py",
    "content": "\"\"\"Icon with text component.\"\"\"\n\nfrom ....core.rich_component import RichComponent, ComponentType\n\n\nclass IconTextComponent(RichComponent):\n    \"\"\"Simple component for displaying an icon with text.\"\"\"\n\n    type: ComponentType = ComponentType.ICON_TEXT\n    icon: str\n    text: str\n    variant: str = \"default\"  # \"default\", \"primary\", \"secondary\", \"muted\"\n    size: str = \"medium\"  # \"small\", \"medium\", \"large\"\n    alignment: str = \"left\"  # \"left\", \"center\", \"right\"\n"
  },
  {
    "path": "src/vanna/components/rich/feedback/log_viewer.py",
    "content": "\"\"\"Log viewer component.\"\"\"\n\nimport uuid\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional\nfrom pydantic import BaseModel, Field\nfrom ....core.rich_component import RichComponent, ComponentType\n\n\nclass LogEntry(BaseModel):\n    \"\"\"Log entry for tool execution.\"\"\"\n\n    timestamp: str = Field(default_factory=lambda: datetime.utcnow().isoformat())\n    level: str = \"info\"  # \"debug\", \"info\", \"warning\", \"error\"\n    message: str\n    data: Optional[Dict[str, Any]] = None\n\n\nclass LogViewerComponent(RichComponent):\n    \"\"\"Generic log viewer for displaying timestamped entries.\"\"\"\n\n    type: ComponentType = ComponentType.LOG_VIEWER\n    title: str = \"Logs\"\n    entries: List[LogEntry] = Field(default_factory=list)\n    max_entries: int = 100\n    searchable: bool = True\n    show_timestamps: bool = True\n    auto_scroll: bool = True\n\n    def add_entry(\n        self, message: str, level: str = \"info\", data: Optional[Dict[str, Any]] = None\n    ) -> \"LogViewerComponent\":\n        \"\"\"Add a new log entry.\"\"\"\n        new_entry = LogEntry(message=message, level=level, data=data)\n        new_entries = self.entries + [new_entry]\n\n        # Limit to max_entries\n        if len(new_entries) > self.max_entries:\n            new_entries = new_entries[-self.max_entries :]\n\n        return self.update(entries=new_entries)\n"
  },
  {
    "path": "src/vanna/components/rich/feedback/notification.py",
    "content": "\"\"\"Notification component for alerts and messages.\"\"\"\n\nfrom typing import Any, Dict, List, Optional\nfrom pydantic import Field\nfrom ....core.rich_component import RichComponent, ComponentType\n\n\nclass NotificationComponent(RichComponent):\n    \"\"\"Notification component for alerts and messages.\"\"\"\n\n    type: ComponentType = ComponentType.NOTIFICATION\n    message: str\n    title: Optional[str] = None\n    level: str = \"info\"  # \"success\", \"info\", \"warning\", \"error\"\n    icon: Optional[str] = None\n    dismissible: bool = True\n    auto_dismiss: bool = False\n    auto_dismiss_delay: int = 5000  # milliseconds\n    actions: List[Dict[str, Any]] = Field(default_factory=list)\n"
  },
  {
    "path": "src/vanna/components/rich/feedback/progress.py",
    "content": "\"\"\"Progress components for displaying progress indicators.\"\"\"\n\nfrom typing import Any, Dict, Optional\nfrom ....core.rich_component import RichComponent, ComponentType\n\n\nclass ProgressBarComponent(RichComponent):\n    \"\"\"Progress bar with status and value.\"\"\"\n\n    type: ComponentType = ComponentType.PROGRESS_BAR\n    value: float  # 0.0 to 1.0\n    label: Optional[str] = None\n    show_percentage: bool = True\n    status: Optional[str] = None  # \"success\", \"warning\", \"error\"\n    animated: bool = False\n\n\nclass ProgressDisplayComponent(RichComponent):\n    \"\"\"Generic progress display for any long-running process.\"\"\"\n\n    type: ComponentType = ComponentType.PROGRESS_DISPLAY\n    label: str\n    value: float = 0.0  # 0.0 to 1.0\n    description: Optional[str] = None\n    status: Optional[str] = None  # \"info\", \"success\", \"warning\", \"error\"\n    show_percentage: bool = True\n    animated: bool = False\n    indeterminate: bool = False\n\n    def update_progress(\n        self, value: float, description: Optional[str] = None\n    ) -> \"ProgressDisplayComponent\":\n        \"\"\"Update progress value and optionally description.\"\"\"\n        updates: Dict[str, Any] = {\"value\": max(0.0, min(1.0, value))}\n        if description is not None:\n            updates[\"description\"] = description\n        return self.update(**updates)\n"
  },
  {
    "path": "src/vanna/components/rich/feedback/status_card.py",
    "content": "\"\"\"Status card component for displaying process status.\"\"\"\n\nfrom typing import Any, Dict, List, Optional\nfrom pydantic import Field\nfrom ....core.rich_component import RichComponent, ComponentType\n\n\nclass StatusCardComponent(RichComponent):\n    \"\"\"Generic status card that can display any process status.\"\"\"\n\n    type: ComponentType = ComponentType.STATUS_CARD\n    title: str\n    status: str  # \"pending\", \"running\", \"completed\", \"failed\", \"success\", \"warning\", \"error\"\n    description: Optional[str] = None\n    icon: Optional[str] = None\n    metadata: Dict[str, Any] = Field(default_factory=dict)\n    actions: List[Dict[str, Any]] = Field(default_factory=list)\n    collapsible: bool = False\n    collapsed: bool = False\n\n    def set_status(\n        self, status: str, description: Optional[str] = None\n    ) -> \"StatusCardComponent\":\n        \"\"\"Update the status and optionally the description.\"\"\"\n        updates = {\"status\": status}\n        if description is not None:\n            updates[\"description\"] = description\n        return self.update(**updates)\n"
  },
  {
    "path": "src/vanna/components/rich/feedback/status_indicator.py",
    "content": "\"\"\"Status indicator component.\"\"\"\n\nfrom typing import Optional\nfrom ....core.rich_component import RichComponent, ComponentType\n\n\nclass StatusIndicatorComponent(RichComponent):\n    \"\"\"Status indicator with icon and message.\"\"\"\n\n    type: ComponentType = ComponentType.STATUS_INDICATOR\n    status: str  # \"success\", \"warning\", \"error\", \"info\", \"loading\"\n    message: str\n    icon: Optional[str] = None\n    pulse: bool = False\n"
  },
  {
    "path": "src/vanna/components/rich/interactive/__init__.py",
    "content": "\"\"\"Interactive components.\"\"\"\n\nfrom .task_list import TaskListComponent, Task\nfrom .ui_state import (\n    StatusBarUpdateComponent,\n    TaskTrackerUpdateComponent,\n    ChatInputUpdateComponent,\n    TaskOperation,\n)\nfrom .button import ButtonComponent, ButtonGroupComponent\n\n__all__ = [\n    \"TaskListComponent\",\n    \"Task\",\n    \"StatusBarUpdateComponent\",\n    \"TaskTrackerUpdateComponent\",\n    \"ChatInputUpdateComponent\",\n    \"TaskOperation\",\n    \"ButtonComponent\",\n    \"ButtonGroupComponent\",\n]\n"
  },
  {
    "path": "src/vanna/components/rich/interactive/button.py",
    "content": "\"\"\"Button component for interactive actions.\"\"\"\n\nfrom typing import Any, Dict, List, Literal, Optional\nfrom ....core.rich_component import ComponentType, RichComponent\n\n\nclass ButtonComponent(RichComponent):\n    \"\"\"Interactive button that sends a message when clicked.\n\n    The button renders in the UI and when clicked, sends its action\n    value as a message to the chat input.\n\n    Args:\n        label: Text displayed on the button\n        action: Message/command to send when clicked\n        variant: Visual style variant\n        size: Button size\n        icon: Optional emoji or icon\n        icon_position: Position of icon relative to label\n        disabled: Whether button is disabled\n\n    Example:\n        ButtonComponent(\n            label=\"Generate Report\",\n            action=\"/report sales\",\n            variant=\"primary\",\n            icon=\"📊\"\n        )\n    \"\"\"\n\n    def __init__(\n        self,\n        label: str,\n        action: str,\n        variant: Literal[\n            \"primary\", \"secondary\", \"success\", \"warning\", \"error\", \"ghost\", \"link\"\n        ] = \"primary\",\n        size: Literal[\"small\", \"medium\", \"large\"] = \"medium\",\n        icon: Optional[str] = None,\n        icon_position: Literal[\"left\", \"right\"] = \"left\",\n        disabled: bool = False,\n    ):\n        super().__init__(\n            type=ComponentType.BUTTON,\n            data={\n                \"label\": label,\n                \"action\": action,\n                \"variant\": variant,\n                \"size\": size,\n                \"icon\": icon,\n                \"icon_position\": icon_position,\n                \"disabled\": disabled,\n            },\n        )\n\n\nclass ButtonGroupComponent(RichComponent):\n    \"\"\"Group of buttons with consistent styling.\n\n    Args:\n        buttons: List of button data dictionaries\n        orientation: Layout direction\n        spacing: Gap between buttons\n        alignment: Button alignment within group\n        full_width: Whether buttons should stretch to fill width\n\n    Example:\n        ButtonGroupComponent(\n            buttons=[\n                {\"label\": \"Yes\", \"action\": \"/confirm yes\", \"variant\": \"success\"},\n                {\"label\": \"No\", \"action\": \"/confirm no\", \"variant\": \"error\"},\n            ],\n            orientation=\"horizontal\",\n            spacing=\"medium\"\n        )\n    \"\"\"\n\n    def __init__(\n        self,\n        buttons: List[Dict[str, Any]],\n        orientation: Literal[\"horizontal\", \"vertical\"] = \"horizontal\",\n        spacing: Literal[\"small\", \"medium\", \"large\"] = \"medium\",\n        alignment: Literal[\"start\", \"center\", \"end\", \"stretch\"] = \"start\",\n        full_width: bool = False,\n    ):\n        super().__init__(\n            type=ComponentType.BUTTON_GROUP,\n            data={\n                \"buttons\": buttons,\n                \"orientation\": orientation,\n                \"spacing\": spacing,\n                \"alignment\": alignment,\n                \"full_width\": full_width,\n            },\n        )\n"
  },
  {
    "path": "src/vanna/components/rich/interactive/task_list.py",
    "content": "\"\"\"Task list component for interactive task tracking.\"\"\"\n\nimport uuid\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional\nfrom pydantic import BaseModel, Field\nfrom ....core.rich_component import RichComponent, ComponentType\n\n\nclass Task(BaseModel):\n    \"\"\"Individual task in a task list.\"\"\"\n\n    id: str = Field(default_factory=lambda: str(uuid.uuid4()))\n    title: str\n    description: Optional[str] = None\n    status: str = \"pending\"  # \"pending\", \"in_progress\", \"completed\", \"error\"\n    progress: Optional[float] = None  # 0.0 to 1.0\n    created_at: str = Field(default_factory=lambda: datetime.utcnow().isoformat())\n    completed_at: Optional[str] = None\n    metadata: Dict[str, Any] = Field(default_factory=dict)\n\n\nclass TaskListComponent(RichComponent):\n    \"\"\"Interactive task list with progress tracking.\"\"\"\n\n    type: ComponentType = ComponentType.TASK_LIST\n    title: str = \"Tasks\"\n    tasks: List[Task] = Field(default_factory=list)\n    show_progress: bool = True\n    allow_reorder: bool = False\n    show_timestamps: bool = True\n    filter_status: Optional[str] = None  # Filter by task status\n\n    def add_task(self, task: Task) -> \"TaskListComponent\":\n        \"\"\"Add a task to the list.\"\"\"\n        new_tasks = self.tasks + [task]\n        return self.update(tasks=new_tasks)\n\n    def update_task(self, task_id: str, **updates: Any) -> \"TaskListComponent\":\n        \"\"\"Update a specific task.\"\"\"\n        new_tasks = []\n        for task in self.tasks:\n            if task.id == task_id:\n                task_data = task.model_dump()\n                task_data.update(updates)\n                new_tasks.append(Task(**task_data))\n            else:\n                new_tasks.append(task)\n        return self.update(tasks=new_tasks)\n\n    def complete_task(self, task_id: str) -> \"TaskListComponent\":\n        \"\"\"Mark a task as completed.\"\"\"\n        return self.update_task(\n            task_id,\n            status=\"completed\",\n            completed_at=datetime.utcnow().isoformat(),\n            progress=1.0,\n        )\n"
  },
  {
    "path": "src/vanna/components/rich/interactive/ui_state.py",
    "content": "\"\"\"UI state update components for controlling interface elements.\"\"\"\n\nfrom enum import Enum\nfrom typing import Any, Optional\nfrom .task_list import Task\nfrom ....core.rich_component import RichComponent, ComponentType\n\n\nclass StatusBarUpdateComponent(RichComponent):\n    \"\"\"Component for updating the status bar above chat input.\"\"\"\n\n    type: ComponentType = ComponentType.STATUS_BAR_UPDATE\n    status: str  # \"idle\", \"working\", \"success\", \"error\"\n    message: str\n    detail: Optional[str] = None\n\n    def __init__(self, **kwargs: Any) -> None:\n        # Set a fixed ID for status bar updates\n        kwargs.setdefault(\"id\", \"vanna-status-bar\")\n        super().__init__(**kwargs)\n\n\nclass TaskOperation(str, Enum):\n    \"\"\"Operations for task tracker updates.\"\"\"\n\n    ADD_TASK = \"add_task\"\n    UPDATE_TASK = \"update_task\"\n    REMOVE_TASK = \"remove_task\"\n    CLEAR_TASKS = \"clear_tasks\"\n\n\nclass TaskTrackerUpdateComponent(RichComponent):\n    \"\"\"Component for updating the task tracker in the sidebar.\"\"\"\n\n    type: ComponentType = ComponentType.TASK_TRACKER_UPDATE\n    operation: TaskOperation\n    task: Optional[Task] = None  # Used for ADD_TASK\n    task_id: Optional[str] = None  # Used for UPDATE_TASK and REMOVE_TASK\n    status: Optional[str] = None  # Used for UPDATE_TASK\n    progress: Optional[float] = None  # Used for UPDATE_TASK\n    detail: Optional[str] = None  # Used for UPDATE_TASK\n\n    def __init__(self, **kwargs: Any) -> None:\n        # Set a fixed ID for task tracker updates\n        kwargs.setdefault(\"id\", \"vanna-task-tracker\")\n        super().__init__(**kwargs)\n\n    @classmethod\n    def add_task(cls, task: Task) -> \"TaskTrackerUpdateComponent\":\n        \"\"\"Create a component to add a new task.\"\"\"\n        return cls(operation=TaskOperation.ADD_TASK, task=task)\n\n    @classmethod\n    def update_task(\n        cls,\n        task_id: str,\n        status: Optional[str] = None,\n        progress: Optional[float] = None,\n        detail: Optional[str] = None,\n    ) -> \"TaskTrackerUpdateComponent\":\n        \"\"\"Create a component to update an existing task.\"\"\"\n        return cls(\n            operation=TaskOperation.UPDATE_TASK,\n            task_id=task_id,\n            status=status,\n            progress=progress,\n            detail=detail,\n        )\n\n    @classmethod\n    def remove_task(cls, task_id: str) -> \"TaskTrackerUpdateComponent\":\n        \"\"\"Create a component to remove a task.\"\"\"\n        return cls(operation=TaskOperation.REMOVE_TASK, task_id=task_id)\n\n    @classmethod\n    def clear_tasks(cls) -> \"TaskTrackerUpdateComponent\":\n        \"\"\"Create a component to clear all tasks.\"\"\"\n        return cls(operation=TaskOperation.CLEAR_TASKS)\n\n\nclass ChatInputUpdateComponent(RichComponent):\n    \"\"\"Component for updating chat input state and appearance.\"\"\"\n\n    type: ComponentType = ComponentType.CHAT_INPUT_UPDATE\n    placeholder: Optional[str] = None\n    disabled: Optional[bool] = None\n    value: Optional[str] = None  # Set input text value\n    focus: Optional[bool] = None  # Focus/unfocus the input\n\n    def __init__(self, **kwargs: Any) -> None:\n        # Set a fixed ID for chat input updates\n        kwargs.setdefault(\"id\", \"vanna-chat-input\")\n        super().__init__(**kwargs)\n"
  },
  {
    "path": "src/vanna/components/rich/specialized/__init__.py",
    "content": "\"\"\"Specialized components.\"\"\"\n\nfrom .artifact import ArtifactComponent\n\n__all__ = [\n    \"ArtifactComponent\",\n]\n"
  },
  {
    "path": "src/vanna/components/rich/specialized/artifact.py",
    "content": "\"\"\"Artifact component for interactive content.\"\"\"\n\nimport uuid\nfrom typing import Optional\nfrom pydantic import Field\nfrom ....core.rich_component import RichComponent, ComponentType\n\n\nclass ArtifactComponent(RichComponent):\n    \"\"\"Component for displaying interactive artifacts that can be rendered externally.\"\"\"\n\n    type: ComponentType = ComponentType.ARTIFACT\n    artifact_id: str = Field(default_factory=lambda: f\"artifact_{uuid.uuid4().hex[:8]}\")\n    content: str  # HTML/SVG/JS content\n    artifact_type: str  # \"html\", \"svg\", \"visualization\", \"interactive\", \"d3\", \"threejs\"\n    title: Optional[str] = None\n    description: Optional[str] = None\n    editable: bool = True\n    fullscreen_capable: bool = True\n    external_renderable: bool = True\n"
  },
  {
    "path": "src/vanna/components/rich/text.py",
    "content": "\"\"\"Rich text component.\"\"\"\n\nfrom typing import Optional\nfrom ...core.rich_component import RichComponent, ComponentType\n\n\nclass RichTextComponent(RichComponent):\n    \"\"\"Rich text component with formatting options.\"\"\"\n\n    type: ComponentType = ComponentType.TEXT\n    content: str\n    markdown: bool = False\n    code_language: Optional[str] = None  # For syntax highlighting\n    font_size: Optional[str] = None\n    font_weight: Optional[str] = None\n    text_align: Optional[str] = None\n"
  },
  {
    "path": "src/vanna/components/simple/__init__.py",
    "content": "\"\"\"Simple UI components for basic rendering.\"\"\"\n\n# Import from core\nfrom ...core.simple_component import SimpleComponent, SimpleComponentType\nfrom .text import SimpleTextComponent\nfrom .image import SimpleImageComponent\nfrom .link import SimpleLinkComponent\n\n__all__ = [\n    \"SimpleComponent\",\n    \"SimpleComponentType\",\n    \"SimpleTextComponent\",\n    \"SimpleImageComponent\",\n    \"SimpleLinkComponent\",\n]\n"
  },
  {
    "path": "src/vanna/components/simple/image.py",
    "content": "\"\"\"Simple image component.\"\"\"\n\nfrom typing import Optional\nfrom pydantic import Field\nfrom ...core.simple_component import SimpleComponent, SimpleComponentType\n\n\nclass SimpleImageComponent(SimpleComponent):\n    \"\"\"A simple image component.\"\"\"\n\n    type: SimpleComponentType = SimpleComponentType.IMAGE\n    url: str = Field(..., description=\"The URL of the image to display.\")\n    alt_text: Optional[str] = Field(\n        default=None, description=\"Alternative text for the image.\"\n    )\n"
  },
  {
    "path": "src/vanna/components/simple/link.py",
    "content": "\"\"\"Simple link component.\"\"\"\n\nfrom typing import Optional\nfrom pydantic import Field\nfrom ...core.simple_component import SimpleComponent, SimpleComponentType\n\n\nclass SimpleLinkComponent(SimpleComponent):\n    \"\"\"A simple link component.\"\"\"\n\n    type: SimpleComponentType = SimpleComponentType.LINK\n    url: str = Field(..., description=\"The URL the link points to.\")\n    text: Optional[str] = Field(\n        default=None, description=\"The display text for the link.\"\n    )\n"
  },
  {
    "path": "src/vanna/components/simple/text.py",
    "content": "\"\"\"Simple text component.\"\"\"\n\nfrom pydantic import Field\nfrom ...core.simple_component import SimpleComponent, SimpleComponentType\n\n\nclass SimpleTextComponent(SimpleComponent):\n    \"\"\"A simple text component.\"\"\"\n\n    type: SimpleComponentType = SimpleComponentType.TEXT\n    text: str = Field(..., description=\"The text content to display.\")\n"
  },
  {
    "path": "src/vanna/core/__init__.py",
    "content": "\"\"\"\nCore components of the Vanna Agents framework.\n\nThis package contains the fundamental abstractions and implementations\nthat form the foundation of the agent framework.\n\"\"\"\n\n# Core domains - re-export from new structure\nfrom .tool import T, Tool, ToolCall, ToolContext, ToolResult, ToolSchema\nfrom .llm import LlmMessage, LlmRequest, LlmResponse, LlmService, LlmStreamChunk\nfrom .storage import Conversation, ConversationStore, Message\nfrom .user import User, UserService\nfrom .agent import Agent, AgentConfig\nfrom .system_prompt import DefaultSystemPromptBuilder, SystemPromptBuilder\nfrom .lifecycle import LifecycleHook\nfrom .middleware import LlmMiddleware\nfrom .workflow import WorkflowHandler, WorkflowResult, DefaultWorkflowHandler\nfrom .recovery import ErrorRecoveryStrategy, RecoveryAction, RecoveryActionType\nfrom .enricher import ToolContextEnricher\nfrom .enhancer import LlmContextEnhancer, DefaultLlmContextEnhancer\nfrom .filter import ConversationFilter\nfrom .observability import ObservabilityProvider, Span, Metric\nfrom .audit import (\n    AuditLogger,\n    AuditEvent,\n    AuditEventType,\n    ToolAccessCheckEvent,\n    ToolInvocationEvent,\n    ToolResultEvent,\n    UiFeatureAccessCheckEvent,\n    AiResponseEvent,\n)\n\n# UI Components\nfrom .components import UiComponent\nfrom .rich_component import RichComponent\nfrom ..components import (\n    SimpleComponent,\n    SimpleComponentType,\n    SimpleImageComponent,\n    SimpleLinkComponent,\n    SimpleTextComponent,\n    ArtifactComponent,\n    BadgeComponent,\n    CardComponent,\n    DataFrameComponent,\n    IconTextComponent,\n    LogViewerComponent,\n    NotificationComponent,\n    ProgressBarComponent,\n    ProgressDisplayComponent,\n    RichTextComponent,\n    StatusCardComponent,\n    TaskListComponent,\n)\n\n# Exceptions\nfrom .errors import (\n    AgentError,\n    ConversationNotFoundError,\n    LlmServiceError,\n    PermissionError,\n    ToolExecutionError,\n    ToolNotFoundError,\n    ValidationError,\n)\n\n# Core implementations\nfrom .registry import ToolRegistry\n\n# Evaluation framework\nfrom .evaluation import (\n    Evaluator,\n    TestCase,\n    ExpectedOutcome,\n    AgentResult,\n    EvaluationResult,\n    TestCaseResult,\n    AgentVariant,\n    EvaluationRunner,\n    TrajectoryEvaluator,\n    OutputEvaluator,\n    LLMAsJudgeEvaluator,\n    EfficiencyEvaluator,\n    EvaluationReport,\n    ComparisonReport,\n    EvaluationDataset,\n)\n\n# Rebuild models to resolve forward references after all imports\nfrom .tool.models import ToolContext, ToolResult\nfrom .components import UiComponent  # Import UiComponent to ensure it's available\n\nToolContext.model_rebuild()\nToolResult.model_rebuild()\n\n__all__ = [\n    # Models\n    \"User\",\n    \"Message\",\n    \"Conversation\",\n    \"ToolCall\",\n    \"ToolResult\",\n    \"ToolContext\",\n    \"ToolSchema\",\n    \"LlmMessage\",\n    \"LlmRequest\",\n    \"LlmResponse\",\n    \"LlmStreamChunk\",\n    \"RecoveryAction\",\n    \"RecoveryActionType\",\n    \"Span\",\n    \"Metric\",\n    # Interfaces\n    \"Tool\",\n    \"Agent\",\n    \"LlmService\",\n    \"ConversationStore\",\n    \"UserService\",\n    \"SystemPromptBuilder\",\n    \"LifecycleHook\",\n    \"LlmMiddleware\",\n    \"WorkflowHandler\",\n    \"DefaultWorkflowHandler\",\n    \"WorkflowResult\",\n    \"ErrorRecoveryStrategy\",\n    \"ToolContextEnricher\",\n    \"LlmContextEnhancer\",\n    \"DefaultLlmContextEnhancer\",\n    \"ConversationFilter\",\n    \"ObservabilityProvider\",\n    \"AuditLogger\",\n    \"T\",\n    # Audit\n    \"AuditEvent\",\n    \"AuditEventType\",\n    \"ToolAccessCheckEvent\",\n    \"ToolInvocationEvent\",\n    \"ToolResultEvent\",\n    \"UiFeatureAccessCheckEvent\",\n    \"AiResponseEvent\",\n    # UI Components\n    \"UiComponent\",\n    # Simple Components\n    \"SimpleComponent\",\n    \"SimpleComponentType\",\n    \"SimpleTextComponent\",\n    \"SimpleImageComponent\",\n    \"SimpleLinkComponent\",\n    # Rich Components\n    \"RichComponent\",\n    \"ArtifactComponent\",\n    \"BadgeComponent\",\n    \"CardComponent\",\n    \"DataFrameComponent\",\n    \"IconTextComponent\",\n    \"LogViewerComponent\",\n    \"NotificationComponent\",\n    \"ProgressBarComponent\",\n    \"ProgressDisplayComponent\",\n    \"RichTextComponent\",\n    \"StatusCardComponent\",\n    \"TaskListComponent\",\n    # Core implementations\n    \"ToolRegistry\",\n    \"Agent\",\n    \"AgentConfig\",\n    \"DefaultSystemPromptBuilder\",\n    # Evaluation\n    \"Evaluator\",\n    \"TestCase\",\n    \"ExpectedOutcome\",\n    \"AgentResult\",\n    \"EvaluationResult\",\n    \"TestCaseResult\",\n    \"AgentVariant\",\n    \"EvaluationRunner\",\n    \"TrajectoryEvaluator\",\n    \"OutputEvaluator\",\n    \"LLMAsJudgeEvaluator\",\n    \"EfficiencyEvaluator\",\n    \"EvaluationReport\",\n    \"ComparisonReport\",\n    \"EvaluationDataset\",\n    # Exceptions\n    \"AgentError\",\n    \"ToolExecutionError\",\n    \"ToolNotFoundError\",\n    \"PermissionError\",\n    \"ConversationNotFoundError\",\n    \"LlmServiceError\",\n    \"ValidationError\",\n]\n"
  },
  {
    "path": "src/vanna/core/_compat.py",
    "content": "\"\"\"\nCompatibility shims for different Python versions.\n\nThis module provides compatibility utilities for features that vary across\nPython versions.\n\"\"\"\n\ntry:\n    from enum import StrEnum  # Py 3.11+\nexcept ImportError:  # Py < 3.11\n    from enum import Enum\n\n    class StrEnum(str, Enum):  # type: ignore[no-redef]\n        \"\"\"Minimal backport of StrEnum for Python < 3.11.\"\"\"\n\n        pass\n\n\n__all__ = [\"StrEnum\"]\n"
  },
  {
    "path": "src/vanna/core/agent/__init__.py",
    "content": "\"\"\"\nAgent module.\n\nThis module contains the core Agent implementation and configuration.\n\"\"\"\n\nfrom .agent import Agent\nfrom .config import AgentConfig\n\n__all__ = [\"Agent\", \"AgentConfig\"]\n"
  },
  {
    "path": "src/vanna/core/agent/agent.py",
    "content": "\"\"\"\nAgent implementation for the Vanna Agents framework.\n\nThis module provides the main Agent class that orchestrates the interaction\nbetween LLM services, tools, and conversation storage.\n\"\"\"\n\nimport traceback\nimport uuid\nfrom typing import TYPE_CHECKING, AsyncGenerator, List, Optional\n\nfrom vanna.components import (\n    UiComponent,\n    SimpleTextComponent,\n    RichTextComponent,\n    StatusBarUpdateComponent,\n    TaskTrackerUpdateComponent,\n    ChatInputUpdateComponent,\n    StatusCardComponent,\n    Task,\n)\nfrom .config import AgentConfig\nfrom vanna.core.storage import ConversationStore\nfrom vanna.core.llm import LlmService\nfrom vanna.core.system_prompt import SystemPromptBuilder\nfrom vanna.core.storage import Conversation, Message\nfrom vanna.core.llm import LlmMessage, LlmRequest, LlmResponse\nfrom vanna.core.tool import ToolCall, ToolContext, ToolResult, ToolSchema\nfrom vanna.core.user import User\nfrom vanna.core.registry import ToolRegistry\nfrom vanna.core.system_prompt import DefaultSystemPromptBuilder\nfrom vanna.core.lifecycle import LifecycleHook\nfrom vanna.core.middleware import LlmMiddleware\nfrom vanna.core.workflow import WorkflowHandler, DefaultWorkflowHandler\nfrom vanna.core.recovery import ErrorRecoveryStrategy, RecoveryActionType\nfrom vanna.core.enricher import ToolContextEnricher\nfrom vanna.core.enhancer import LlmContextEnhancer, DefaultLlmContextEnhancer\nfrom vanna.core.filter import ConversationFilter\nfrom vanna.core.observability import ObservabilityProvider\nfrom vanna.core.user.resolver import UserResolver\nfrom vanna.core.user.request_context import RequestContext\nfrom vanna.core.agent.config import UiFeature\nfrom vanna.core.audit import AuditLogger\nfrom vanna.capabilities.agent_memory import AgentMemory\n\nimport logging\n\nlogger = logging.getLogger(__name__)\n\nlogger.info(\"Loaded vanna.core.agent.agent module\")\n\nif TYPE_CHECKING:\n    pass\n\n\nclass Agent:\n    \"\"\"Main agent implementation.\n\n    The Agent class orchestrates LLM interactions, tool execution, and conversation\n    management. It provides 7 extensibility points for customization:\n\n    - lifecycle_hooks: Hook into message and tool execution lifecycle\n    - llm_middlewares: Intercept and transform LLM requests/responses\n    - error_recovery_strategy: Handle errors with retry logic\n    - context_enrichers: Add data to tool execution context\n    - llm_context_enhancer: Enhance LLM system prompts and messages with context\n    - conversation_filters: Filter conversation history before LLM calls\n    - observability_provider: Collect telemetry and monitoring data\n\n    Example:\n        agent = Agent(\n            llm_service=AnthropicLlmService(api_key=\"...\"),\n            tool_registry=registry,\n            conversation_store=store,\n            lifecycle_hooks=[QuotaCheckHook()],\n            llm_middlewares=[CachingMiddleware()],\n            llm_context_enhancer=DefaultLlmContextEnhancer(agent_memory),\n            observability_provider=LoggingProvider()\n        )\n    \"\"\"\n\n    def __init__(\n        self,\n        llm_service: LlmService,\n        tool_registry: ToolRegistry,\n        user_resolver: UserResolver,\n        agent_memory: AgentMemory,\n        conversation_store: Optional[ConversationStore] = None,\n        config: AgentConfig = AgentConfig(),\n        system_prompt_builder: SystemPromptBuilder = DefaultSystemPromptBuilder(),\n        lifecycle_hooks: List[LifecycleHook] = [],\n        llm_middlewares: List[LlmMiddleware] = [],\n        workflow_handler: Optional[WorkflowHandler] = None,\n        error_recovery_strategy: Optional[ErrorRecoveryStrategy] = None,\n        context_enrichers: List[ToolContextEnricher] = [],\n        llm_context_enhancer: Optional[LlmContextEnhancer] = None,\n        conversation_filters: List[ConversationFilter] = [],\n        observability_provider: Optional[ObservabilityProvider] = None,\n        audit_logger: Optional[AuditLogger] = None,\n    ):\n        self.llm_service = llm_service\n        self.tool_registry = tool_registry\n        self.user_resolver = user_resolver\n        self.agent_memory = agent_memory\n\n        # Import here to avoid circular dependency\n        if conversation_store is None:\n            from vanna.integrations.local import MemoryConversationStore\n\n            conversation_store = MemoryConversationStore()\n\n        self.conversation_store = conversation_store\n        self.config = config\n        self.system_prompt_builder = system_prompt_builder\n        self.lifecycle_hooks = lifecycle_hooks\n        self.llm_middlewares = llm_middlewares\n\n        # Use DefaultWorkflowHandler if none provided\n        if workflow_handler is None:\n            workflow_handler = DefaultWorkflowHandler()\n        self.workflow_handler = workflow_handler\n\n        self.error_recovery_strategy = error_recovery_strategy\n        self.context_enrichers = context_enrichers\n\n        # Use DefaultLlmContextEnhancer if none provided\n        if llm_context_enhancer is None:\n            llm_context_enhancer = DefaultLlmContextEnhancer(agent_memory)\n        self.llm_context_enhancer = llm_context_enhancer\n\n        self.conversation_filters = conversation_filters\n        self.observability_provider = observability_provider\n        self.audit_logger = audit_logger\n\n        # Wire audit logger into tool registry\n        if self.audit_logger and self.config.audit_config.enabled:\n            self.tool_registry.audit_logger = self.audit_logger\n            self.tool_registry.audit_config = self.config.audit_config\n\n        logger.info(\"Initialized Agent\")\n\n    async def send_message(\n        self,\n        request_context: RequestContext,\n        message: str,\n        *,\n        conversation_id: Optional[str] = None,\n    ) -> AsyncGenerator[UiComponent, None]:\n        \"\"\"\n        Process a user message and yield UI components with error handling.\n\n        Args:\n            request_context: Request context for user resolution (includes metadata)\n            message: User's message content\n            conversation_id: Optional conversation ID; if None, creates new conversation\n\n        Yields:\n            UiComponent instances for UI updates\n        \"\"\"\n        try:\n            # Delegate to internal method\n            async for component in self._send_message(\n                request_context, message, conversation_id=conversation_id\n            ):\n                yield component\n        except Exception as e:\n            # Log full stack trace\n            stack_trace = traceback.format_exc()\n            logger.error(\n                f\"Error in send_message (conversation_id={conversation_id}): {e}\\n{stack_trace}\",\n                exc_info=True,\n            )\n\n            # Log to observability provider if available\n            if self.observability_provider:\n                try:\n                    error_span = await self.observability_provider.create_span(\n                        \"agent.send_message.error\",\n                        attributes={\n                            \"error_type\": type(e).__name__,\n                            \"error_message\": str(e),\n                            \"conversation_id\": conversation_id or \"none\",\n                        },\n                    )\n                    await self.observability_provider.end_span(error_span)\n                    await self.observability_provider.record_metric(\n                        \"agent.error.count\",\n                        1.0,\n                        \"count\",\n                        tags={\"error_type\": type(e).__name__},\n                    )\n                except Exception as obs_error:\n                    logger.error(\n                        f\"Failed to log error to observability provider: {obs_error}\",\n                        exc_info=True,\n                    )\n\n            # Yield error component to UI (simple, user-friendly message)\n            error_description = \"An unexpected error occurred while processing your message. Please try again.\"\n            if conversation_id:\n                error_description += f\"\\n\\nConversation ID: {conversation_id}\"\n\n            yield UiComponent(\n                rich_component=StatusCardComponent(\n                    title=\"Error Processing Message\",\n                    status=\"error\",\n                    description=error_description,\n                    icon=\"⚠️\",\n                ),\n                simple_component=SimpleTextComponent(\n                    text=f\"Error: An unexpected error occurred. Please try again.{f' (Conversation ID: {conversation_id})' if conversation_id else ''}\"\n                ),\n            )\n\n            # Update status bar to show error state\n            yield UiComponent(  # type: ignore\n                rich_component=StatusBarUpdateComponent(\n                    status=\"error\",\n                    message=\"Error occurred\",\n                    detail=\"An unexpected error occurred while processing your message\",\n                )\n            )\n\n            # Re-enable chat input so user can try again\n            yield UiComponent(  # type: ignore\n                rich_component=ChatInputUpdateComponent(\n                    placeholder=\"Try again...\", disabled=False\n                )\n            )\n\n    async def _send_message(\n        self,\n        request_context: RequestContext,\n        message: str,\n        *,\n        conversation_id: Optional[str] = None,\n    ) -> AsyncGenerator[UiComponent, None]:\n        \"\"\"\n        Internal method to process a user message and yield UI components.\n\n        Args:\n            request_context: Request context for user resolution (includes metadata)\n            message: User's message content\n            conversation_id: Optional conversation ID; if None, creates new conversation\n\n        Yields:\n            UiComponent instances for UI updates\n        \"\"\"\n        # Resolve user from request context with observability\n        user_resolution_span = None\n        if self.observability_provider:\n            user_resolution_span = await self.observability_provider.create_span(\n                \"agent.user_resolution\",\n                attributes={\"has_context\": request_context is not None},\n            )\n\n        user = await self.user_resolver.resolve_user(request_context)\n\n        if self.observability_provider and user_resolution_span:\n            user_resolution_span.set_attribute(\"user_id\", user.id)\n            await self.observability_provider.end_span(user_resolution_span)\n            if user_resolution_span.duration_ms():\n                await self.observability_provider.record_metric(\n                    \"agent.user_resolution.duration\",\n                    user_resolution_span.duration_ms() or 0,\n                    \"ms\",\n                )\n\n        # Check if this is a starter UI request (empty message or explicit metadata flag)\n        is_starter_request = (not message.strip()) or request_context.metadata.get(\n            \"starter_ui_request\", False\n        )\n\n        if is_starter_request and self.workflow_handler:\n            # Handle starter UI request with observability\n            starter_span = None\n            if self.observability_provider:\n                starter_span = await self.observability_provider.create_span(\n                    \"agent.workflow_handler.starter_ui\", attributes={\"user_id\": user.id}\n                )\n\n            try:\n                # Load or create conversation for context\n                if conversation_id is None:\n                    conversation_id = str(uuid.uuid4())\n\n                conversation = await self.conversation_store.get_conversation(\n                    conversation_id, user\n                )\n                if not conversation:\n                    # Create empty conversation (will be saved if workflow produces components)\n                    conversation = Conversation(\n                        id=conversation_id, user=user, messages=[]\n                    )\n\n                # Get starter UI from workflow handler\n                components = await self.workflow_handler.get_starter_ui(\n                    self, user, conversation\n                )\n\n                if self.observability_provider and starter_span:\n                    starter_span.set_attribute(\"has_components\", components is not None)\n                    starter_span.set_attribute(\n                        \"component_count\", len(components) if components else 0\n                    )\n\n                if components:\n                    # Yield the starter UI components\n                    for component in components:\n                        yield component\n\n                    # Yield finalization components\n                    yield UiComponent(  # type: ignore\n                        rich_component=StatusBarUpdateComponent(\n                            status=\"idle\",\n                            message=\"Ready\",\n                            detail=\"Choose an option or type a message\",\n                        )\n                    )\n                    yield UiComponent(  # type: ignore\n                        rich_component=ChatInputUpdateComponent(\n                            placeholder=\"Ask a question...\", disabled=False\n                        )\n                    )\n\n                if self.observability_provider and starter_span:\n                    await self.observability_provider.end_span(starter_span)\n                    if starter_span.duration_ms():\n                        await self.observability_provider.record_metric(\n                            \"agent.workflow_handler.starter_ui.duration\",\n                            starter_span.duration_ms() or 0,\n                            \"ms\",\n                        )\n\n                # Save the conversation if it was newly created\n                if self.config.auto_save_conversations:\n                    await self.conversation_store.update_conversation(conversation)\n\n                return  # Exit without calling LLM\n\n            except Exception as e:\n                logger.error(f\"Error generating starter UI: {e}\", exc_info=True)\n                if self.observability_provider and starter_span:\n                    starter_span.set_attribute(\"error\", str(e))\n                    await self.observability_provider.end_span(starter_span)\n                # Fall through to normal processing on error\n\n        # Don't process actual empty messages (that aren't starter requests)\n        if not message.strip():\n            return\n\n        # Create observability span for entire message processing\n        message_span = None\n        if self.observability_provider:\n            message_span = await self.observability_provider.create_span(\n                \"agent.send_message\",\n                attributes={\n                    \"user_id\": user.id,\n                    \"conversation_id\": conversation_id or \"new\",\n                },\n            )\n\n        # Run before_message hooks with observability\n        modified_message = message\n        for hook in self.lifecycle_hooks:\n            hook_span = None\n            if self.observability_provider:\n                hook_span = await self.observability_provider.create_span(\n                    \"agent.hook.before_message\",\n                    attributes={\"hook\": hook.__class__.__name__},\n                )\n\n            hook_result = await hook.before_message(user, modified_message)\n            if hook_result is not None:\n                modified_message = hook_result\n\n            if self.observability_provider and hook_span:\n                hook_span.set_attribute(\"modified_message\", hook_result is not None)\n                await self.observability_provider.end_span(hook_span)\n                if hook_span.duration_ms():\n                    await self.observability_provider.record_metric(\n                        \"agent.hook.duration\",\n                        hook_span.duration_ms() or 0,\n                        \"ms\",\n                        tags={\n                            \"hook\": hook.__class__.__name__,\n                            \"phase\": \"before_message\",\n                        },\n                    )\n\n        # Use the potentially modified message\n        message = modified_message\n\n        # Generate conversation ID and request ID if not provided\n        if conversation_id is None:\n            conversation_id = str(uuid.uuid4())\n\n        request_id = str(uuid.uuid4())\n\n        # Update status to working\n        yield UiComponent(  # type: ignore\n            rich_component=StatusBarUpdateComponent(\n                status=\"working\",\n                message=\"Processing your request...\",\n                detail=\"Analyzing query\",\n            )\n        )\n\n        # Load or create conversation with observability (but don't add message yet)\n        conversation_span = None\n        if self.observability_provider:\n            conversation_span = await self.observability_provider.create_span(\n                \"agent.conversation.load\",\n                attributes={\"conversation_id\": conversation_id, \"user_id\": user.id},\n            )\n\n        conversation = await self.conversation_store.get_conversation(\n            conversation_id, user\n        )\n\n        is_new_conversation = conversation is None\n\n        if not conversation:\n            # Create empty conversation (will add message after workflow handler check)\n            conversation = Conversation(id=conversation_id, user=user, messages=[])\n\n        if self.observability_provider and conversation_span:\n            conversation_span.set_attribute(\"is_new\", is_new_conversation)\n            conversation_span.set_attribute(\"message_count\", len(conversation.messages))\n            await self.observability_provider.end_span(conversation_span)\n            if conversation_span.duration_ms():\n                await self.observability_provider.record_metric(\n                    \"agent.conversation.load.duration\",\n                    conversation_span.duration_ms() or 0,\n                    \"ms\",\n                    tags={\"is_new\": str(is_new_conversation)},\n                )\n\n        # Try workflow handler before adding message to conversation\n        if self.workflow_handler:\n            trigger_span = None\n            if self.observability_provider:\n                trigger_span = await self.observability_provider.create_span(\n                    \"agent.workflow_handler.try_handle\",\n                    attributes={\"user_id\": user.id, \"conversation_id\": conversation_id},\n                )\n\n            try:\n                workflow_result = await self.workflow_handler.try_handle(\n                    self, user, conversation, message\n                )\n\n                if self.observability_provider and trigger_span:\n                    trigger_span.set_attribute(\n                        \"should_skip_llm\", workflow_result.should_skip_llm\n                    )\n\n                if workflow_result.should_skip_llm:\n                    # Workflow handled the message, short-circuit LLM\n\n                    # Apply conversation mutation if provided\n                    if workflow_result.conversation_mutation:\n                        await workflow_result.conversation_mutation(conversation)\n\n                    # Stream components\n                    if workflow_result.components:\n                        if isinstance(workflow_result.components, list):\n                            for component in workflow_result.components:\n                                yield component\n                        else:\n                            # AsyncGenerator\n                            async for component in workflow_result.components:\n                                yield component\n\n                    # Finalize response (status bar + chat input)\n                    yield UiComponent(  # type: ignore\n                        rich_component=StatusBarUpdateComponent(\n                            status=\"idle\",\n                            message=\"Workflow complete\",\n                            detail=\"Ready for next message\",\n                        )\n                    )\n                    yield UiComponent(  # type: ignore\n                        rich_component=ChatInputUpdateComponent(\n                            placeholder=\"Ask a question...\", disabled=False\n                        )\n                    )\n\n                    # Save conversation if auto-save enabled\n                    if self.config.auto_save_conversations:\n                        await self.conversation_store.update_conversation(conversation)\n\n                    if self.observability_provider and trigger_span:\n                        await self.observability_provider.end_span(trigger_span)\n\n                    # Exit without calling LLM\n                    return\n\n            except Exception as e:\n                logger.error(f\"Error in workflow handler: {e}\", exc_info=True)\n                if self.observability_provider and trigger_span:\n                    trigger_span.set_attribute(\"error\", str(e))\n                    await self.observability_provider.end_span(trigger_span)\n                # Fall through to normal LLM processing on error\n\n            finally:\n                if self.observability_provider and trigger_span:\n                    await self.observability_provider.end_span(trigger_span)\n\n        # Persist new conversation to store before adding message\n        if is_new_conversation:\n            await self.conversation_store.update_conversation(conversation)\n\n        # Not triggered, add user message to conversation now\n        conversation.add_message(Message(role=\"user\", content=message))\n\n        # Add initial task\n        context_task = Task(\n            title=\"Load conversation context\",\n            description=\"Reading message history and user context\",\n            status=\"pending\",\n        )\n        yield UiComponent(  # type: ignore\n            rich_component=TaskTrackerUpdateComponent.add_task(context_task)\n        )\n\n        # Collect available UI features for auditing\n        ui_features_available = []\n        for feature_name in self.config.ui_features.feature_group_access.keys():\n            if self.config.ui_features.can_user_access_feature(feature_name, user):\n                ui_features_available.append(feature_name)\n\n        # Create context with observability provider and UI features\n        context = ToolContext(\n            user=user,\n            conversation_id=conversation_id,\n            request_id=request_id,\n            agent_memory=self.agent_memory,\n            observability_provider=self.observability_provider,\n            metadata={\"ui_features_available\": ui_features_available},\n        )\n\n        # Enrich context with additional data with observability\n        for enricher in self.context_enrichers:\n            enrichment_span = None\n            if self.observability_provider:\n                enrichment_span = await self.observability_provider.create_span(\n                    \"agent.context.enrichment\",\n                    attributes={\"enricher\": enricher.__class__.__name__},\n                )\n\n            context = await enricher.enrich_context(context)\n\n            if self.observability_provider and enrichment_span:\n                await self.observability_provider.end_span(enrichment_span)\n                if enrichment_span.duration_ms():\n                    await self.observability_provider.record_metric(\n                        \"agent.enrichment.duration\",\n                        enrichment_span.duration_ms() or 0,\n                        \"ms\",\n                        tags={\"enricher\": enricher.__class__.__name__},\n                    )\n\n        # Get available tools for user with observability\n        schema_span = None\n        if self.observability_provider:\n            schema_span = await self.observability_provider.create_span(\n                \"agent.tool_schemas.fetch\", attributes={\"user_id\": user.id}\n            )\n\n        tool_schemas = await self.tool_registry.get_schemas(user)\n\n        if self.observability_provider and schema_span:\n            schema_span.set_attribute(\"schema_count\", len(tool_schemas))\n            await self.observability_provider.end_span(schema_span)\n            if schema_span.duration_ms():\n                await self.observability_provider.record_metric(\n                    \"agent.tool_schemas.duration\",\n                    schema_span.duration_ms() or 0,\n                    \"ms\",\n                    tags={\"schema_count\": str(len(tool_schemas))},\n                )\n\n        # Update task status to completed\n        yield UiComponent(  # type: ignore\n            rich_component=TaskTrackerUpdateComponent.update_task(\n                context_task.id, status=\"completed\"\n            )\n        )\n\n        # Build system prompt with observability\n        prompt_span = None\n        if self.observability_provider:\n            prompt_span = await self.observability_provider.create_span(\n                \"agent.system_prompt.build\",\n                attributes={\"tool_count\": len(tool_schemas)},\n            )\n\n        system_prompt = await self.system_prompt_builder.build_system_prompt(\n            user, tool_schemas\n        )\n\n        # Enhance system prompt with LLM context enhancer\n        if self.llm_context_enhancer and system_prompt is not None:\n            enhancement_span = None\n            if self.observability_provider:\n                enhancement_span = await self.observability_provider.create_span(\n                    \"agent.llm_context.enhance_system_prompt\",\n                    attributes={\n                        \"enhancer\": self.llm_context_enhancer.__class__.__name__\n                    },\n                )\n\n            system_prompt = await self.llm_context_enhancer.enhance_system_prompt(\n                system_prompt, message, user\n            )\n\n            if self.observability_provider and enhancement_span:\n                await self.observability_provider.end_span(enhancement_span)\n                if enhancement_span.duration_ms():\n                    await self.observability_provider.record_metric(\n                        \"agent.llm_context.enhance_system_prompt.duration\",\n                        enhancement_span.duration_ms() or 0,\n                        \"ms\",\n                        tags={\"enhancer\": self.llm_context_enhancer.__class__.__name__},\n                    )\n\n        if self.observability_provider and prompt_span:\n            prompt_span.set_attribute(\n                \"prompt_length\", len(system_prompt) if system_prompt else 0\n            )\n            await self.observability_provider.end_span(prompt_span)\n            if prompt_span.duration_ms():\n                await self.observability_provider.record_metric(\n                    \"agent.system_prompt.duration\", prompt_span.duration_ms() or 0, \"ms\"\n                )\n\n        # Build LLM request\n        request = await self._build_llm_request(\n            conversation, tool_schemas, user, system_prompt\n        )\n\n        # Process with tool loop\n        tool_iterations = 0\n\n        while tool_iterations < self.config.max_tool_iterations:\n            if self.config.include_thinking_indicators and tool_iterations == 0:\n                # TODO: Yield thinking indicator\n                pass\n\n            # Get LLM response\n            if self.config.stream_responses:\n                response = await self._handle_streaming_response(request)\n            else:\n                response = await self._send_llm_request(request)\n\n            # Handle tool calls\n            if response.is_tool_call():\n                tool_iterations += 1\n\n                # First, add the assistant message with tool_calls to the conversation\n                # This is required for OpenAI API - tool messages must follow assistant messages with tool_calls\n                assistant_message = Message(\n                    role=\"assistant\",\n                    content=response.content or \"\",  # Ensure content is not None\n                    tool_calls=response.tool_calls,\n                )\n                conversation.add_message(assistant_message)\n\n                if response.content is not None:\n                    # Yield any partial content from the assistant before tool execution\n                    has_tool_invocation_message_in_chat = (\n                        self.config.ui_features.can_user_access_feature(\n                            UiFeature.UI_FEATURE_SHOW_TOOL_INVOCATION_MESSAGE_IN_CHAT,\n                            user,\n                        )\n                    )\n                    if has_tool_invocation_message_in_chat:\n                        yield UiComponent(\n                            rich_component=RichTextComponent(\n                                content=response.content, markdown=True\n                            ),\n                            simple_component=SimpleTextComponent(text=response.content),\n                        )\n\n                        # Update status to executing tools\n                        yield UiComponent(  # type: ignore\n                            rich_component=StatusBarUpdateComponent(\n                                status=\"working\",\n                                message=\"Executing tools...\",\n                                detail=f\"Running {len(response.tool_calls or [])} tools\",\n                            )\n                        )\n                    else:\n                        # Yield as a status update instead\n                        yield UiComponent(  # type: ignore\n                            rich_component=StatusBarUpdateComponent(\n                                status=\"working\", message=response.content, detail=\"\"\n                            )\n                        )\n\n                # Collect all tool results first\n                tool_results = []\n                for i, tool_call in enumerate(response.tool_calls or []):\n                    # Add task for this tool execution\n                    tool_task = Task(\n                        title=f\"Execute {tool_call.name}\",\n                        description=f\"Running tool with provided arguments\",\n                        status=\"in_progress\",\n                    )\n\n                    has_tool_names_access = (\n                        self.config.ui_features.can_user_access_feature(\n                            UiFeature.UI_FEATURE_SHOW_TOOL_NAMES, user\n                        )\n                    )\n\n                    # Audit UI feature access check\n                    if (\n                        self.audit_logger\n                        and self.config.audit_config.enabled\n                        and self.config.audit_config.log_ui_feature_checks\n                    ):\n                        await self.audit_logger.log_ui_feature_access(\n                            user=user,\n                            feature_name=UiFeature.UI_FEATURE_SHOW_TOOL_NAMES,\n                            access_granted=has_tool_names_access,\n                            required_groups=self.config.ui_features.feature_group_access.get(\n                                UiFeature.UI_FEATURE_SHOW_TOOL_NAMES, []\n                            ),\n                            conversation_id=conversation.id,\n                            request_id=request_id,\n                        )\n\n                    if has_tool_names_access:\n                        yield UiComponent(  # type: ignore\n                            rich_component=TaskTrackerUpdateComponent.add_task(\n                                tool_task\n                            )\n                        )\n\n                    response_str = response.content\n\n                    # Use primitive StatusCard instead of semantic ToolExecutionComponent\n                    tool_status_card = StatusCardComponent(\n                        title=f\"Executing {tool_call.name}\",\n                        status=\"running\",\n                        description=f\"Running tool with {len(tool_call.arguments)} arguments\",\n                        icon=\"⚙️\",\n                        metadata=tool_call.arguments,\n                    )\n\n                    has_tool_args_access = (\n                        self.config.ui_features.can_user_access_feature(\n                            UiFeature.UI_FEATURE_SHOW_TOOL_ARGUMENTS, user\n                        )\n                    )\n\n                    # Audit UI feature access check\n                    if (\n                        self.audit_logger\n                        and self.config.audit_config.enabled\n                        and self.config.audit_config.log_ui_feature_checks\n                    ):\n                        await self.audit_logger.log_ui_feature_access(\n                            user=user,\n                            feature_name=UiFeature.UI_FEATURE_SHOW_TOOL_ARGUMENTS,\n                            access_granted=has_tool_args_access,\n                            required_groups=self.config.ui_features.feature_group_access.get(\n                                UiFeature.UI_FEATURE_SHOW_TOOL_ARGUMENTS, []\n                            ),\n                            conversation_id=conversation.id,\n                            request_id=request_id,\n                        )\n\n                    if has_tool_args_access:\n                        yield UiComponent(\n                            rich_component=tool_status_card,\n                            simple_component=SimpleTextComponent(\n                                text=response_str or \"\"\n                            ),\n                        )\n\n                    # Run before_tool hooks with observability\n                    tool = await self.tool_registry.get_tool(tool_call.name)\n                    if tool:\n                        for hook in self.lifecycle_hooks:\n                            hook_span = None\n                            if self.observability_provider:\n                                hook_span = (\n                                    await self.observability_provider.create_span(\n                                        \"agent.hook.before_tool\",\n                                        attributes={\n                                            \"hook\": hook.__class__.__name__,\n                                            \"tool\": tool_call.name,\n                                        },\n                                    )\n                                )\n\n                            await hook.before_tool(tool, context)\n\n                            if self.observability_provider and hook_span:\n                                await self.observability_provider.end_span(hook_span)\n                                if hook_span.duration_ms():\n                                    await self.observability_provider.record_metric(\n                                        \"agent.hook.duration\",\n                                        hook_span.duration_ms() or 0,\n                                        \"ms\",\n                                        tags={\n                                            \"hook\": hook.__class__.__name__,\n                                            \"phase\": \"before_tool\",\n                                            \"tool\": tool_call.name,\n                                        },\n                                    )\n\n                    # Execute tool with observability\n                    tool_exec_span = None\n                    if self.observability_provider:\n                        tool_exec_span = await self.observability_provider.create_span(\n                            \"agent.tool.execute\",\n                            attributes={\n                                \"tool\": tool_call.name,\n                                \"arg_count\": len(tool_call.arguments),\n                            },\n                        )\n\n                    result = await self.tool_registry.execute(tool_call, context)\n\n                    if self.observability_provider and tool_exec_span:\n                        tool_exec_span.set_attribute(\"success\", result.success)\n                        if not result.success:\n                            tool_exec_span.set_attribute(\n                                \"error\", result.error or \"unknown\"\n                            )\n                        await self.observability_provider.end_span(tool_exec_span)\n                        if tool_exec_span.duration_ms():\n                            await self.observability_provider.record_metric(\n                                \"agent.tool.duration\",\n                                tool_exec_span.duration_ms() or 0,\n                                \"ms\",\n                                tags={\n                                    \"tool\": tool_call.name,\n                                    \"success\": str(result.success),\n                                },\n                            )\n\n                    # Run after_tool hooks with observability\n                    for hook in self.lifecycle_hooks:\n                        hook_span = None\n                        if self.observability_provider:\n                            hook_span = await self.observability_provider.create_span(\n                                \"agent.hook.after_tool\",\n                                attributes={\n                                    \"hook\": hook.__class__.__name__,\n                                    \"tool\": tool_call.name,\n                                },\n                            )\n\n                        modified_result = await hook.after_tool(result)\n                        if modified_result is not None:\n                            result = modified_result\n\n                        if self.observability_provider and hook_span:\n                            hook_span.set_attribute(\n                                \"modified_result\", modified_result is not None\n                            )\n                            await self.observability_provider.end_span(hook_span)\n                            if hook_span.duration_ms():\n                                await self.observability_provider.record_metric(\n                                    \"agent.hook.duration\",\n                                    hook_span.duration_ms() or 0,\n                                    \"ms\",\n                                    tags={\n                                        \"hook\": hook.__class__.__name__,\n                                        \"phase\": \"after_tool\",\n                                        \"tool\": tool_call.name,\n                                    },\n                                )\n\n                    # Update status card to show completion\n                    final_status = \"success\" if result.success else \"error\"\n                    final_description = (\n                        f\"Tool completed successfully\"\n                        if result.success\n                        else f\"Tool failed: {result.error or 'Unknown error'}\"\n                    )\n\n                    has_tool_args_access_2 = (\n                        self.config.ui_features.can_user_access_feature(\n                            UiFeature.UI_FEATURE_SHOW_TOOL_ARGUMENTS, user\n                        )\n                    )\n\n                    # Audit UI feature access check\n                    if (\n                        self.audit_logger\n                        and self.config.audit_config.enabled\n                        and self.config.audit_config.log_ui_feature_checks\n                    ):\n                        await self.audit_logger.log_ui_feature_access(\n                            user=user,\n                            feature_name=UiFeature.UI_FEATURE_SHOW_TOOL_ARGUMENTS,\n                            access_granted=has_tool_args_access_2,\n                            required_groups=self.config.ui_features.feature_group_access.get(\n                                UiFeature.UI_FEATURE_SHOW_TOOL_ARGUMENTS, []\n                            ),\n                            conversation_id=conversation.id,\n                            request_id=request_id,\n                        )\n\n                    if has_tool_args_access_2:\n                        yield UiComponent(\n                            rich_component=tool_status_card.set_status(\n                                final_status, final_description\n                            ),\n                            simple_component=SimpleTextComponent(\n                                text=final_description\n                            ),\n                        )\n\n                    has_tool_names_access_2 = (\n                        self.config.ui_features.can_user_access_feature(\n                            UiFeature.UI_FEATURE_SHOW_TOOL_NAMES, user\n                        )\n                    )\n\n                    # Audit UI feature access check\n                    if (\n                        self.audit_logger\n                        and self.config.audit_config.enabled\n                        and self.config.audit_config.log_ui_feature_checks\n                    ):\n                        await self.audit_logger.log_ui_feature_access(\n                            user=user,\n                            feature_name=UiFeature.UI_FEATURE_SHOW_TOOL_NAMES,\n                            access_granted=has_tool_names_access_2,\n                            required_groups=self.config.ui_features.feature_group_access.get(\n                                UiFeature.UI_FEATURE_SHOW_TOOL_NAMES, []\n                            ),\n                            conversation_id=conversation.id,\n                            request_id=request_id,\n                        )\n\n                    if has_tool_names_access_2:\n                        # Update tool task to completed\n                        yield UiComponent(  # type: ignore\n                            rich_component=TaskTrackerUpdateComponent.update_task(\n                                tool_task.id,\n                                status=\"completed\",\n                                detail=f\"Tool {'completed successfully' if result.success else 'return an error'}\",\n                            )\n                        )\n\n                    # Yield tool result\n                    if result.ui_component:\n                        # For errors, check if user has access to see error details\n                        if not result.success:\n                            has_tool_error_access = (\n                                self.config.ui_features.can_user_access_feature(\n                                    UiFeature.UI_FEATURE_SHOW_TOOL_ERROR, user\n                                )\n                            )\n\n                            # Audit UI feature access check\n                            if (\n                                self.audit_logger\n                                and self.config.audit_config.enabled\n                                and self.config.audit_config.log_ui_feature_checks\n                            ):\n                                await self.audit_logger.log_ui_feature_access(\n                                    user=user,\n                                    feature_name=UiFeature.UI_FEATURE_SHOW_TOOL_ERROR,\n                                    access_granted=has_tool_error_access,\n                                    required_groups=self.config.ui_features.feature_group_access.get(\n                                        UiFeature.UI_FEATURE_SHOW_TOOL_ERROR, []\n                                    ),\n                                    conversation_id=conversation.id,\n                                    request_id=request_id,\n                                )\n\n                            if has_tool_error_access:\n                                yield result.ui_component\n                        else:\n                            # Success results are always shown if they exist\n                            yield result.ui_component\n\n                    # Collect tool result data\n                    tool_results.append(\n                        {\n                            \"tool_call_id\": tool_call.id,\n                            \"content\": (\n                                result.result_for_llm\n                                if result.success\n                                else result.error or \"Tool execution failed\"\n                            ),\n                        }\n                    )\n\n                # Add tool responses to conversation\n                # For APIs that need all tool results in one message, this helps\n                for tool_result in tool_results:\n                    tool_response_message = Message(\n                        role=\"tool\",\n                        content=tool_result[\"content\"],\n                        tool_call_id=tool_result[\"tool_call_id\"],\n                    )\n                    conversation.add_message(tool_response_message)\n\n                # Rebuild request with tool responses\n                request = await self._build_llm_request(\n                    conversation, tool_schemas, user, system_prompt\n                )\n            else:\n                # Update status to idle and set completion message\n                yield UiComponent(  # type: ignore\n                    rich_component=StatusBarUpdateComponent(\n                        status=\"idle\",\n                        message=\"Response complete\",\n                        detail=\"Ready for next message\",\n                    )\n                )\n\n                # Update chat input placeholder\n                yield UiComponent(  # type: ignore\n                    rich_component=ChatInputUpdateComponent(\n                        placeholder=\"Ask a follow-up question...\", disabled=False\n                    )\n                )\n\n                # Yield final text response\n                if response.content:\n                    # Add assistant response to conversation\n                    conversation.add_message(\n                        Message(role=\"assistant\", content=response.content)\n                    )\n                    yield UiComponent(\n                        rich_component=RichTextComponent(\n                            content=response.content, markdown=True\n                        ),\n                        simple_component=SimpleTextComponent(text=response.content),\n                    )\n                break\n\n        # Check if we hit the tool iteration limit\n        if tool_iterations >= self.config.max_tool_iterations:\n            # The loop exited due to hitting the limit, not due to a natural completion\n            logger.warning(\n                f\"Tool iteration limit reached: {tool_iterations}/{self.config.max_tool_iterations}\"\n            )\n\n            # Update status bar to show warning\n            yield UiComponent(  # type: ignore\n                rich_component=StatusBarUpdateComponent(\n                    status=\"warning\",\n                    message=\"Tool limit reached\",\n                    detail=f\"Stopped after {tool_iterations} tool executions. The task may be incomplete.\",\n                )\n            )\n\n            # Provide detailed warning message to user\n            warning_message = f\"\"\"⚠️ **Tool Execution Limit Reached**\n\nThe agent stopped after executing {tool_iterations} tools (the configured maximum). The task may not be fully complete.\n\nYou can:\n- Ask me to continue where I left off\n- Adjust the `max_tool_iterations` setting if you need more tool calls\n- Break the task into smaller steps\"\"\"\n\n            yield UiComponent(\n                rich_component=RichTextComponent(\n                    content=warning_message, markdown=True\n                ),\n                simple_component=SimpleTextComponent(\n                    text=f\"Tool limit reached after {tool_iterations} executions. Task may be incomplete.\"\n                ),\n            )\n\n            # Update chat input to suggest follow-up\n            yield UiComponent(  # type: ignore\n                rich_component=ChatInputUpdateComponent(\n                    placeholder=\"Continue the task or ask me something else...\",\n                    disabled=False,\n                )\n            )\n\n        # Save conversation if configured\n        if self.config.auto_save_conversations:\n            save_span = None\n            if self.observability_provider:\n                save_span = await self.observability_provider.create_span(\n                    \"agent.conversation.save\",\n                    attributes={\n                        \"conversation_id\": conversation_id,\n                        \"message_count\": len(conversation.messages),\n                    },\n                )\n\n            await self.conversation_store.update_conversation(conversation)\n\n            if self.observability_provider and save_span:\n                await self.observability_provider.end_span(save_span)\n                if save_span.duration_ms():\n                    await self.observability_provider.record_metric(\n                        \"agent.conversation.save.duration\",\n                        save_span.duration_ms() or 0,\n                        \"ms\",\n                    )\n\n        # Run after_message hooks with observability\n        for hook in self.lifecycle_hooks:\n            hook_span = None\n            if self.observability_provider:\n                hook_span = await self.observability_provider.create_span(\n                    \"agent.hook.after_message\",\n                    attributes={\"hook\": hook.__class__.__name__},\n                )\n\n            await hook.after_message(conversation)\n\n            if self.observability_provider and hook_span:\n                await self.observability_provider.end_span(hook_span)\n                if hook_span.duration_ms():\n                    await self.observability_provider.record_metric(\n                        \"agent.hook.duration\",\n                        hook_span.duration_ms() or 0,\n                        \"ms\",\n                        tags={\n                            \"hook\": hook.__class__.__name__,\n                            \"phase\": \"after_message\",\n                        },\n                    )\n\n        # End observability span and record metrics\n        if self.observability_provider and message_span:\n            message_span.set_attribute(\"tool_iterations\", tool_iterations)\n\n            # Track if we hit the tool iteration limit\n            hit_tool_limit = tool_iterations >= self.config.max_tool_iterations\n            message_span.set_attribute(\"hit_tool_limit\", hit_tool_limit)\n            if hit_tool_limit:\n                message_span.set_attribute(\"incomplete_response\", True)\n                logger.info(\n                    f\"Tool limit reached - marking response as potentially incomplete\"\n                )\n\n            await self.observability_provider.end_span(message_span)\n            if message_span.duration_ms():\n                await self.observability_provider.record_metric(\n                    \"agent.message.duration\",\n                    message_span.duration_ms() or 0,\n                    \"ms\",\n                    tags={\"user_id\": user.id, \"hit_tool_limit\": str(hit_tool_limit)},\n                )\n\n    async def get_available_tools(self, user: User) -> List[ToolSchema]:\n        \"\"\"Get tools available to the user.\"\"\"\n        return await self.tool_registry.get_schemas(user)\n\n    async def _build_llm_request(\n        self,\n        conversation: Conversation,\n        tool_schemas: List[ToolSchema],\n        user: User,\n        system_prompt: Optional[str] = None,\n    ) -> LlmRequest:\n        \"\"\"Build LLM request from conversation and tools.\"\"\"\n        # Apply conversation filters with observability\n        filtered_messages = conversation.messages\n        for filter in self.conversation_filters:\n            filter_span = None\n            if self.observability_provider:\n                filter_span = await self.observability_provider.create_span(\n                    \"agent.conversation.filter\",\n                    attributes={\n                        \"filter\": filter.__class__.__name__,\n                        \"message_count_before\": len(filtered_messages),\n                    },\n                )\n\n            filtered_messages = await filter.filter_messages(filtered_messages)\n\n            if self.observability_provider and filter_span:\n                filter_span.set_attribute(\"message_count_after\", len(filtered_messages))\n                await self.observability_provider.end_span(filter_span)\n                if filter_span.duration_ms():\n                    await self.observability_provider.record_metric(\n                        \"agent.filter.duration\",\n                        filter_span.duration_ms() or 0,\n                        \"ms\",\n                        tags={\"filter\": filter.__class__.__name__},\n                    )\n\n        messages = []\n        for msg in filtered_messages:\n            llm_msg = LlmMessage(\n                role=msg.role,\n                content=msg.content,\n                tool_calls=msg.tool_calls,\n                tool_call_id=msg.tool_call_id,\n            )\n            messages.append(llm_msg)\n\n        # Enhance messages with LLM context enhancer\n        if self.llm_context_enhancer:\n            enhancement_span = None\n            if self.observability_provider:\n                enhancement_span = await self.observability_provider.create_span(\n                    \"agent.llm_context.enhance_user_messages\",\n                    attributes={\n                        \"enhancer\": self.llm_context_enhancer.__class__.__name__,\n                        \"message_count\": len(messages),\n                    },\n                )\n\n            messages = await self.llm_context_enhancer.enhance_user_messages(\n                messages, user\n            )\n\n            if self.observability_provider and enhancement_span:\n                enhancement_span.set_attribute(\"message_count_after\", len(messages))\n                await self.observability_provider.end_span(enhancement_span)\n                if enhancement_span.duration_ms():\n                    await self.observability_provider.record_metric(\n                        \"agent.llm_context.enhance_user_messages.duration\",\n                        enhancement_span.duration_ms() or 0,\n                        \"ms\",\n                        tags={\"enhancer\": self.llm_context_enhancer.__class__.__name__},\n                    )\n\n        return LlmRequest(\n            messages=messages,\n            tools=tool_schemas if tool_schemas else None,\n            user=user,\n            temperature=self.config.temperature,\n            max_tokens=self.config.max_tokens,\n            stream=self.config.stream_responses,\n            system_prompt=system_prompt,\n        )\n\n    async def _send_llm_request(self, request: LlmRequest) -> LlmResponse:\n        \"\"\"Send LLM request with middleware and observability.\"\"\"\n        # Apply before_llm_request middlewares with observability\n        for middleware in self.llm_middlewares:\n            mw_span = None\n            if self.observability_provider:\n                mw_span = await self.observability_provider.create_span(\n                    \"agent.middleware.before_llm\",\n                    attributes={\"middleware\": middleware.__class__.__name__},\n                )\n\n            request = await middleware.before_llm_request(request)\n\n            if self.observability_provider and mw_span:\n                await self.observability_provider.end_span(mw_span)\n                if mw_span.duration_ms():\n                    await self.observability_provider.record_metric(\n                        \"agent.middleware.duration\",\n                        mw_span.duration_ms() or 0,\n                        \"ms\",\n                        tags={\n                            \"middleware\": middleware.__class__.__name__,\n                            \"phase\": \"before_llm\",\n                        },\n                    )\n\n        # Create observability span for LLM call\n        llm_span = None\n        if self.observability_provider:\n            llm_span = await self.observability_provider.create_span(\n                \"llm.request\",\n                attributes={\n                    \"model\": getattr(self.llm_service, \"model\", \"unknown\"),\n                    \"stream\": request.stream,\n                },\n            )\n\n        # Send request\n        response = await self.llm_service.send_request(request)\n\n        # End span and record metrics\n        if self.observability_provider and llm_span:\n            await self.observability_provider.end_span(llm_span)\n            if llm_span.duration_ms():\n                await self.observability_provider.record_metric(\n                    \"llm.request.duration\", llm_span.duration_ms() or 0, \"ms\"\n                )\n\n        # Apply after_llm_response middlewares with observability\n        for middleware in self.llm_middlewares:\n            mw_span = None\n            if self.observability_provider:\n                mw_span = await self.observability_provider.create_span(\n                    \"agent.middleware.after_llm\",\n                    attributes={\"middleware\": middleware.__class__.__name__},\n                )\n\n            response = await middleware.after_llm_response(request, response)\n\n            if self.observability_provider and mw_span:\n                await self.observability_provider.end_span(mw_span)\n                if mw_span.duration_ms():\n                    await self.observability_provider.record_metric(\n                        \"agent.middleware.duration\",\n                        mw_span.duration_ms() or 0,\n                        \"ms\",\n                        tags={\n                            \"middleware\": middleware.__class__.__name__,\n                            \"phase\": \"after_llm\",\n                        },\n                    )\n\n        return response\n\n    async def _handle_streaming_response(self, request: LlmRequest) -> LlmResponse:\n        \"\"\"Handle streaming response from LLM.\"\"\"\n        # Apply before_llm_request middlewares with observability\n        for middleware in self.llm_middlewares:\n            mw_span = None\n            if self.observability_provider:\n                mw_span = await self.observability_provider.create_span(\n                    \"agent.middleware.before_llm\",\n                    attributes={\n                        \"middleware\": middleware.__class__.__name__,\n                        \"stream\": True,\n                    },\n                )\n\n            request = await middleware.before_llm_request(request)\n\n            if self.observability_provider and mw_span:\n                await self.observability_provider.end_span(mw_span)\n                if mw_span.duration_ms():\n                    await self.observability_provider.record_metric(\n                        \"agent.middleware.duration\",\n                        mw_span.duration_ms() or 0,\n                        \"ms\",\n                        tags={\n                            \"middleware\": middleware.__class__.__name__,\n                            \"phase\": \"before_llm\",\n                            \"stream\": \"true\",\n                        },\n                    )\n\n        accumulated_content = \"\"\n        accumulated_tool_calls = []\n\n        # Create span for streaming\n        stream_span = None\n        if self.observability_provider:\n            stream_span = await self.observability_provider.create_span(\n                \"llm.stream\",\n                attributes={\"model\": getattr(self.llm_service, \"model\", \"unknown\")},\n            )\n\n        async for chunk in self.llm_service.stream_request(request):\n            if chunk.content:\n                accumulated_content += chunk.content\n                # Could yield intermediate TextChunk here\n\n            if chunk.tool_calls:\n                accumulated_tool_calls.extend(chunk.tool_calls)\n\n        # End streaming span\n        if self.observability_provider and stream_span:\n            stream_span.set_attribute(\"content_length\", len(accumulated_content))\n            stream_span.set_attribute(\"tool_call_count\", len(accumulated_tool_calls))\n            await self.observability_provider.end_span(stream_span)\n            if stream_span.duration_ms():\n                await self.observability_provider.record_metric(\n                    \"llm.stream.duration\", stream_span.duration_ms() or 0, \"ms\"\n                )\n\n        response = LlmResponse(\n            content=accumulated_content if accumulated_content else None,\n            tool_calls=accumulated_tool_calls if accumulated_tool_calls else None,\n        )\n\n        # Apply after_llm_response middlewares with observability\n        for middleware in self.llm_middlewares:\n            mw_span = None\n            if self.observability_provider:\n                mw_span = await self.observability_provider.create_span(\n                    \"agent.middleware.after_llm\",\n                    attributes={\n                        \"middleware\": middleware.__class__.__name__,\n                        \"stream\": True,\n                    },\n                )\n\n            response = await middleware.after_llm_response(request, response)\n\n            if self.observability_provider and mw_span:\n                await self.observability_provider.end_span(mw_span)\n                if mw_span.duration_ms():\n                    await self.observability_provider.record_metric(\n                        \"agent.middleware.duration\",\n                        mw_span.duration_ms() or 0,\n                        \"ms\",\n                        tags={\n                            \"middleware\": middleware.__class__.__name__,\n                            \"phase\": \"after_llm\",\n                            \"stream\": \"true\",\n                        },\n                    )\n\n        return response\n"
  },
  {
    "path": "src/vanna/core/agent/config.py",
    "content": "\"\"\"\nAgent configuration.\n\nThis module contains configuration models that control agent behavior.\n\"\"\"\n\nfrom typing import TYPE_CHECKING, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field\n\nfrom .._compat import StrEnum\n\nif TYPE_CHECKING:\n    from ..user import User\n\n\nclass UiFeature(StrEnum):\n    UI_FEATURE_SHOW_TOOL_NAMES = \"tool_names\"\n    UI_FEATURE_SHOW_TOOL_ARGUMENTS = \"tool_arguments\"\n    UI_FEATURE_SHOW_TOOL_ERROR = \"tool_error\"\n    UI_FEATURE_SHOW_TOOL_INVOCATION_MESSAGE_IN_CHAT = \"tool_invocation_message_in_chat\"\n    UI_FEATURE_SHOW_MEMORY_DETAILED_RESULTS = \"memory_detailed_results\"\n\n\n# Optional: you can also define defaults if you want a shared baseline\nDEFAULT_UI_FEATURES: Dict[str, List[str]] = {\n    UiFeature.UI_FEATURE_SHOW_TOOL_NAMES: [\"admin\", \"user\"],\n    UiFeature.UI_FEATURE_SHOW_TOOL_ARGUMENTS: [\"admin\"],\n    UiFeature.UI_FEATURE_SHOW_TOOL_ERROR: [\"admin\"],\n    UiFeature.UI_FEATURE_SHOW_TOOL_INVOCATION_MESSAGE_IN_CHAT: [\"admin\"],\n    UiFeature.UI_FEATURE_SHOW_MEMORY_DETAILED_RESULTS: [\"admin\"],\n}\n\n\nclass UiFeatures(BaseModel):\n    \"\"\"UI features with group-based access control using the same pattern as tools.\n\n    Each field specifies which groups can access that UI feature.\n    Empty list means the feature is accessible to all users.\n    Uses the same intersection logic as tool access control.\n    \"\"\"\n\n    # Custom features for extensibility\n    feature_group_access: Dict[str, List[str]] = Field(\n        default_factory=lambda: DEFAULT_UI_FEATURES.copy(),\n        description=\"Which groups can access UI features\",\n    )\n\n    def can_user_access_feature(self, feature_name: str, user: \"User\") -> bool:\n        \"\"\"Check if user can access a UI feature using same logic as tools.\n\n        Args:\n            feature_name: Name of the UI feature to check\n            user: User object with group_memberships\n\n        Returns:\n            True if user has access, False otherwise\n        \"\"\"\n        # Then try custom features\n        if feature_name in self.feature_group_access:\n            allowed_groups = self.feature_group_access[feature_name]\n        else:\n            # Feature doesn't exist, deny access\n            return False\n\n        # Empty list means all users can access (same as tools)\n        if not allowed_groups:\n            return True\n\n        # Same intersection logic as tool access control\n        user_groups = set(user.group_memberships)\n        feature_groups = set(allowed_groups)\n        return bool(user_groups & feature_groups)\n\n    def register_feature(self, name: str, access_groups: List[str]) -> None:\n        \"\"\"Register a custom UI feature with group access control.\n\n        Args:\n            name: Name of the custom feature\n            access_groups: List of groups that can access this feature\n        \"\"\"\n        self.feature_group_access[name] = access_groups\n\n\nclass AuditConfig(BaseModel):\n    \"\"\"Configuration for audit logging.\"\"\"\n\n    enabled: bool = Field(default=True, description=\"Enable audit logging\")\n    log_tool_access_checks: bool = Field(\n        default=True, description=\"Log tool access permission checks\"\n    )\n    log_tool_invocations: bool = Field(\n        default=True, description=\"Log tool invocations with parameters\"\n    )\n    log_tool_results: bool = Field(\n        default=True, description=\"Log tool execution results\"\n    )\n    log_ui_feature_checks: bool = Field(\n        default=False, description=\"Log UI feature access checks (can be noisy)\"\n    )\n    log_ai_responses: bool = Field(\n        default=True, description=\"Log AI-generated responses\"\n    )\n    include_full_ai_responses: bool = Field(\n        default=False,\n        description=\"Include full AI response text in logs (privacy concern)\",\n    )\n    sanitize_tool_parameters: bool = Field(\n        default=True, description=\"Sanitize sensitive parameters (passwords, tokens)\"\n    )\n\n\nclass AgentConfig(BaseModel):\n    \"\"\"Configuration for agent behavior.\"\"\"\n\n    max_tool_iterations: int = Field(default=10, gt=0)\n    stream_responses: bool = Field(default=True)\n    auto_save_conversations: bool = Field(default=True)\n    include_thinking_indicators: bool = Field(default=True)\n    temperature: float = Field(default=0.7, ge=0.0, le=2.0)\n    max_tokens: Optional[int] = Field(default=None, gt=0)\n    ui_features: UiFeatures = Field(default_factory=UiFeatures)\n    audit_config: AuditConfig = Field(default_factory=AuditConfig)\n"
  },
  {
    "path": "src/vanna/core/audit/__init__.py",
    "content": "\"\"\"\nAudit logging for the Vanna Agents framework.\n\nThis module provides interfaces and models for audit logging, enabling\ntracking of user actions, tool invocations, and access control decisions.\n\"\"\"\n\nfrom .base import AuditLogger\nfrom .models import (\n    AiResponseEvent,\n    AuditEvent,\n    AuditEventType,\n    ToolAccessCheckEvent,\n    ToolInvocationEvent,\n    ToolResultEvent,\n    UiFeatureAccessCheckEvent,\n)\n\n__all__ = [\n    \"AuditLogger\",\n    \"AuditEvent\",\n    \"AuditEventType\",\n    \"ToolAccessCheckEvent\",\n    \"ToolInvocationEvent\",\n    \"ToolResultEvent\",\n    \"UiFeatureAccessCheckEvent\",\n    \"AiResponseEvent\",\n]\n"
  },
  {
    "path": "src/vanna/core/audit/base.py",
    "content": "\"\"\"\nBase audit logger interface.\n\nAudit loggers enable tracking user actions, tool invocations, and access control\ndecisions for security, compliance, and debugging.\n\"\"\"\n\nimport hashlib\nfrom abc import ABC, abstractmethod\nfrom datetime import datetime\nfrom typing import TYPE_CHECKING, Any, Dict, List, Optional\n\nfrom .models import (\n    AiResponseEvent,\n    AuditEvent,\n    ToolAccessCheckEvent,\n    ToolInvocationEvent,\n    ToolResultEvent,\n    UiFeatureAccessCheckEvent,\n)\n\nif TYPE_CHECKING:\n    from ..tool.models import ToolCall, ToolContext, ToolResult\n    from ..user.models import User\n\n\nclass AuditLogger(ABC):\n    \"\"\"Abstract base class for audit logging implementations.\n\n    Implementations can:\n    - Write to files (JSON, CSV, etc.)\n    - Send to databases (Postgres, MongoDB, etc.)\n    - Stream to cloud services (CloudWatch, Datadog, etc.)\n    - Send to SIEM systems (Splunk, Elastic, etc.)\n\n    Example:\n        class PostgresAuditLogger(AuditLogger):\n            async def log_event(self, event: AuditEvent) -> None:\n                await self.db.execute(\n                    \"INSERT INTO audit_log (...) VALUES (...)\",\n                    event.model_dump()\n                )\n\n        agent = Agent(\n            llm_service=...,\n            audit_logger=PostgresAuditLogger(db_pool)\n        )\n    \"\"\"\n\n    @abstractmethod\n    async def log_event(self, event: AuditEvent) -> None:\n        \"\"\"Log a single audit event.\n\n        Args:\n            event: The audit event to log\n\n        Raises:\n            Exception: If logging fails critically\n        \"\"\"\n        pass\n\n    async def log_tool_access_check(\n        self,\n        user: \"User\",\n        tool_name: str,\n        access_granted: bool,\n        required_groups: List[str],\n        context: \"ToolContext\",\n        reason: Optional[str] = None,\n    ) -> None:\n        \"\"\"Convenience method for logging tool access checks.\n\n        Args:\n            user: User attempting to access the tool\n            tool_name: Name of the tool being accessed\n            access_granted: Whether access was granted\n            required_groups: Groups required to access the tool\n            context: Tool execution context\n            reason: Optional reason for denial\n        \"\"\"\n        event = ToolAccessCheckEvent(\n            user_id=user.id,\n            username=user.username,\n            user_email=user.email,\n            user_groups=user.group_memberships,\n            conversation_id=context.conversation_id,\n            request_id=context.request_id,\n            tool_name=tool_name,\n            access_granted=access_granted,\n            required_groups=required_groups,\n            reason=reason,\n        )\n        await self.log_event(event)\n\n    async def log_tool_invocation(\n        self,\n        user: \"User\",\n        tool_call: \"ToolCall\",\n        ui_features: List[str],\n        context: \"ToolContext\",\n        sanitize_parameters: bool = True,\n    ) -> None:\n        \"\"\"Convenience method for logging tool invocations.\n\n        Args:\n            user: User invoking the tool\n            tool_call: Tool call information\n            ui_features: List of UI features available to the user\n            context: Tool execution context\n            sanitize_parameters: Whether to sanitize sensitive parameters\n        \"\"\"\n        parameters = tool_call.arguments.copy()\n        sanitized = False\n\n        if sanitize_parameters:\n            parameters, sanitized = self._sanitize_parameters(parameters)\n\n        event = ToolInvocationEvent(\n            user_id=user.id,\n            username=user.username,\n            user_email=user.email,\n            user_groups=user.group_memberships,\n            conversation_id=context.conversation_id,\n            request_id=context.request_id,\n            tool_call_id=tool_call.id,\n            tool_name=tool_call.name,\n            parameters=parameters,\n            parameters_sanitized=sanitized,\n            ui_features_available=ui_features,\n        )\n        await self.log_event(event)\n\n    async def log_tool_result(\n        self,\n        user: \"User\",\n        tool_call: \"ToolCall\",\n        result: \"ToolResult\",\n        context: \"ToolContext\",\n    ) -> None:\n        \"\"\"Convenience method for logging tool results.\n\n        Args:\n            user: User who invoked the tool\n            tool_call: Tool call information\n            result: Tool execution result\n            context: Tool execution context\n        \"\"\"\n        event = ToolResultEvent(\n            user_id=user.id,\n            username=user.username,\n            user_email=user.email,\n            user_groups=user.group_memberships,\n            conversation_id=context.conversation_id,\n            request_id=context.request_id,\n            tool_call_id=tool_call.id,\n            tool_name=tool_call.name,\n            success=result.success,\n            error=result.error,\n            execution_time_ms=result.metadata.get(\"execution_time_ms\", 0.0),\n            result_size_bytes=(\n                len(result.result_for_llm.encode(\"utf-8\"))\n                if result.result_for_llm\n                else 0\n            ),\n            ui_component_type=(\n                result.ui_component.__class__.__name__ if result.ui_component else None\n            ),\n        )\n        await self.log_event(event)\n\n    async def log_ui_feature_access(\n        self,\n        user: \"User\",\n        feature_name: str,\n        access_granted: bool,\n        required_groups: List[str],\n        conversation_id: str,\n        request_id: str,\n    ) -> None:\n        \"\"\"Convenience method for logging UI feature access checks.\n\n        Args:\n            user: User attempting to access the feature\n            feature_name: Name of the UI feature\n            access_granted: Whether access was granted\n            required_groups: Groups required to access the feature\n            conversation_id: Conversation identifier\n            request_id: Request identifier\n        \"\"\"\n        event = UiFeatureAccessCheckEvent(\n            user_id=user.id,\n            username=user.username,\n            user_email=user.email,\n            user_groups=user.group_memberships,\n            conversation_id=conversation_id,\n            request_id=request_id,\n            feature_name=feature_name,\n            access_granted=access_granted,\n            required_groups=required_groups,\n        )\n        await self.log_event(event)\n\n    async def log_ai_response(\n        self,\n        user: \"User\",\n        conversation_id: str,\n        request_id: str,\n        response_text: str,\n        tool_calls: List[\"ToolCall\"],\n        model_info: Optional[Dict[str, Any]] = None,\n        include_full_text: bool = False,\n    ) -> None:\n        \"\"\"Convenience method for logging AI responses.\n\n        Args:\n            user: User receiving the response\n            conversation_id: Conversation identifier\n            request_id: Request identifier\n            response_text: The AI-generated response text\n            tool_calls: List of tool calls in the response\n            model_info: Optional model configuration info\n            include_full_text: Whether to include full response text\n        \"\"\"\n        response_hash = hashlib.sha256(response_text.encode(\"utf-8\")).hexdigest()\n\n        event = AiResponseEvent(\n            user_id=user.id,\n            username=user.username,\n            user_email=user.email,\n            user_groups=user.group_memberships,\n            conversation_id=conversation_id,\n            request_id=request_id,\n            response_length_chars=len(response_text),\n            response_text=response_text if include_full_text else None,\n            response_hash=response_hash,\n            model_name=model_info.get(\"model\") if model_info else None,\n            temperature=model_info.get(\"temperature\") if model_info else None,\n            tool_calls_count=len(tool_calls),\n            tool_names=[tc.name for tc in tool_calls],\n        )\n        await self.log_event(event)\n\n    async def query_events(\n        self,\n        filters: Optional[Dict[str, Any]] = None,\n        start_time: Optional[datetime] = None,\n        end_time: Optional[datetime] = None,\n        limit: int = 100,\n    ) -> List[AuditEvent]:\n        \"\"\"Query audit events (optional, for implementations that support it).\n\n        Args:\n            filters: Filter criteria (user_id, event_type, etc.)\n            start_time: Filter events after this time\n            end_time: Filter events before this time\n            limit: Maximum number of events to return\n\n        Returns:\n            List of matching audit events\n\n        Raises:\n            NotImplementedError: If query not supported by implementation\n        \"\"\"\n        raise NotImplementedError(\"Query not supported by this implementation\")\n\n    def _sanitize_parameters(\n        self, parameters: Dict[str, Any]\n    ) -> tuple[Dict[str, Any], bool]:\n        \"\"\"Sanitize sensitive data from parameters.\n\n        Args:\n            parameters: Raw parameters dict\n\n        Returns:\n            Tuple of (sanitized_parameters, was_sanitized)\n        \"\"\"\n        sanitized = parameters.copy()\n        was_sanitized = False\n\n        # Common sensitive field patterns\n        sensitive_patterns = [\n            \"password\",\n            \"secret\",\n            \"token\",\n            \"api_key\",\n            \"apikey\",\n            \"credential\",\n            \"auth\",\n            \"private_key\",\n            \"access_key\",\n        ]\n\n        for key in list(sanitized.keys()):\n            key_lower = key.lower()\n            if any(pattern in key_lower for pattern in sensitive_patterns):\n                sanitized[key] = \"[REDACTED]\"\n                was_sanitized = True\n\n        return sanitized, was_sanitized\n"
  },
  {
    "path": "src/vanna/core/audit/models.py",
    "content": "\"\"\"\nAudit event models.\n\nThis module contains data models for audit logging events.\n\"\"\"\n\nimport uuid\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field\n\nfrom .._compat import StrEnum\n\n\nclass AuditEventType(StrEnum):\n    \"\"\"Types of audit events.\"\"\"\n\n    # Access control events\n    TOOL_ACCESS_CHECK = \"tool_access_check\"\n    UI_FEATURE_ACCESS_CHECK = \"ui_feature_access_check\"\n\n    # Tool execution events\n    TOOL_INVOCATION = \"tool_invocation\"\n    TOOL_RESULT = \"tool_result\"\n\n    # Conversation events\n    MESSAGE_RECEIVED = \"message_received\"\n    AI_RESPONSE_GENERATED = \"ai_response_generated\"\n    CONVERSATION_CREATED = \"conversation_created\"\n\n    # Security events\n    ACCESS_DENIED = \"access_denied\"\n    AUTHENTICATION_ATTEMPT = \"authentication_attempt\"\n\n\nclass AuditEvent(BaseModel):\n    \"\"\"Base audit event with common fields.\"\"\"\n\n    event_id: str = Field(default_factory=lambda: str(uuid.uuid4()))\n    event_type: AuditEventType\n    timestamp: datetime = Field(default_factory=datetime.utcnow)\n\n    # User context\n    user_id: str\n    username: Optional[str] = None\n    user_email: Optional[str] = None\n    user_groups: List[str] = Field(default_factory=list)\n\n    # Request context\n    conversation_id: str\n    request_id: str\n    remote_addr: Optional[str] = None\n\n    # Event-specific data\n    details: Dict[str, Any] = Field(default_factory=dict)\n\n    # Privacy/redaction markers\n    contains_pii: bool = False\n    redacted_fields: List[str] = Field(default_factory=list)\n\n\nclass ToolAccessCheckEvent(AuditEvent):\n    \"\"\"Audit event for tool access permission checks.\"\"\"\n\n    event_type: AuditEventType = AuditEventType.TOOL_ACCESS_CHECK\n    tool_name: str\n    access_granted: bool\n    required_groups: List[str] = Field(default_factory=list)\n    reason: Optional[str] = None\n\n\nclass ToolInvocationEvent(AuditEvent):\n    \"\"\"Audit event for actual tool executions.\"\"\"\n\n    event_type: AuditEventType = AuditEventType.TOOL_INVOCATION\n    tool_call_id: str\n    tool_name: str\n\n    # Parameters with sanitization support\n    parameters: Dict[str, Any] = Field(default_factory=dict)\n    parameters_sanitized: bool = False\n\n    # UI context at invocation time\n    ui_features_available: List[str] = Field(default_factory=list)\n\n\nclass ToolResultEvent(AuditEvent):\n    \"\"\"Audit event for tool execution results.\"\"\"\n\n    event_type: AuditEventType = AuditEventType.TOOL_RESULT\n    tool_call_id: str\n    tool_name: str\n    success: bool\n    error: Optional[str] = None\n    execution_time_ms: float = 0.0\n\n    # Result metadata (without full content for size)\n    result_size_bytes: Optional[int] = None\n    ui_component_type: Optional[str] = None\n\n\nclass UiFeatureAccessCheckEvent(AuditEvent):\n    \"\"\"Audit event for UI feature access checks.\"\"\"\n\n    event_type: AuditEventType = AuditEventType.UI_FEATURE_ACCESS_CHECK\n    feature_name: str\n    access_granted: bool\n    required_groups: List[str] = Field(default_factory=list)\n\n\nclass AiResponseEvent(AuditEvent):\n    \"\"\"Audit event for AI-generated responses.\"\"\"\n\n    event_type: AuditEventType = AuditEventType.AI_RESPONSE_GENERATED\n\n    # Response metadata\n    response_length_chars: int\n    response_length_tokens: Optional[int] = None\n\n    # Full text (optional, configurable)\n    response_text: Optional[str] = None\n    response_hash: str  # SHA256 for integrity verification\n\n    # Model info\n    model_name: Optional[str] = None\n    temperature: Optional[float] = None\n\n    # Tool calls in response\n    tool_calls_count: int = 0\n    tool_names: List[str] = Field(default_factory=list)\n"
  },
  {
    "path": "src/vanna/core/component_manager.py",
    "content": "\"\"\"\nComponent state management and update protocol for rich components.\n\"\"\"\n\nimport uuid\nfrom datetime import datetime\nfrom enum import Enum\nfrom typing import Any, Dict, List, Optional, Set, Union\n\nfrom pydantic import BaseModel, Field\n\nfrom ..components.rich import ComponentLifecycle, RichComponent\n\n\nclass UpdateOperation(str, Enum):\n    \"\"\"Types of component update operations.\"\"\"\n\n    CREATE = \"create\"\n    UPDATE = \"update\"\n    REPLACE = \"replace\"\n    REMOVE = \"remove\"\n    REORDER = \"reorder\"\n    BULK_UPDATE = \"bulk_update\"\n\n\nclass Position(BaseModel):\n    \"\"\"Position specification for component placement.\"\"\"\n\n    index: Optional[int] = None\n    anchor_id: Optional[str] = None\n    relation: str = \"after\"  # \"before\", \"after\", \"inside\", \"replace\"\n\n\nclass ComponentUpdate(BaseModel):\n    \"\"\"Represents a change to the component tree.\"\"\"\n\n    operation: UpdateOperation\n    target_id: str  # Component being affected\n    component: Optional[RichComponent] = None  # New/updated component data\n    updates: Optional[Dict[str, Any]] = None  # Partial updates for UPDATE operation\n    position: Optional[Position] = None  # For positioning operations\n    timestamp: str = Field(default_factory=lambda: datetime.utcnow().isoformat())\n    batch_id: Optional[str] = None  # For grouping related updates\n\n    def serialize_for_frontend(self) -> Dict[str, Any]:\n        \"\"\"Return update payload with nested components normalized.\"\"\"\n        payload = self.model_dump()\n\n        # Normalise enum values for the frontend contract.\n        payload[\"operation\"] = self.operation.value\n\n        if self.component:\n            payload[\"component\"] = self.component.serialize_for_frontend()\n\n        return payload\n\n\nclass ComponentNode(BaseModel):\n    \"\"\"Node in the component tree.\"\"\"\n\n    component: RichComponent\n    children: List[\"ComponentNode\"] = Field(default_factory=list)\n    parent_id: Optional[str] = None\n\n    def find_child(self, component_id: str) -> Optional[\"ComponentNode\"]:\n        \"\"\"Find a child node by component ID.\"\"\"\n        for child in self.children:\n            if child.component.id == component_id:\n                return child\n            found = child.find_child(component_id)\n            if found:\n                return found\n        return None\n\n    def remove_child(self, component_id: str) -> bool:\n        \"\"\"Remove a child component by ID.\"\"\"\n        for i, child in enumerate(self.children):\n            if child.component.id == component_id:\n                self.children.pop(i)\n                return True\n            if child.remove_child(component_id):\n                return True\n        return False\n\n    def get_all_ids(self) -> Set[str]:\n        \"\"\"Get all component IDs in this subtree.\"\"\"\n        ids = {self.component.id}\n        for child in self.children:\n            ids.update(child.get_all_ids())\n        return ids\n\n\nclass ComponentTree(BaseModel):\n    \"\"\"Hierarchical structure for managing component layout.\"\"\"\n\n    root: Optional[ComponentNode] = None\n    flat_index: Dict[str, ComponentNode] = Field(default_factory=dict)\n\n    def add_component(\n        self, component: RichComponent, position: Optional[Position] = None\n    ) -> ComponentUpdate:\n        \"\"\"Add a component to the tree.\"\"\"\n        node = ComponentNode(component=component)\n        self.flat_index[component.id] = node\n\n        if self.root is None:\n            self.root = node\n        else:\n            parent_node = self._find_parent(position)\n            if parent_node is not None:\n                node.parent_id = parent_node.component.id\n                parent_node.children.append(node)\n\n        return ComponentUpdate(\n            operation=UpdateOperation.CREATE,\n            target_id=component.id,\n            component=component,\n            position=position,\n        )\n\n    def update_component(\n        self, component_id: str, updates: Dict[str, Any]\n    ) -> Optional[ComponentUpdate]:\n        \"\"\"Update a component's properties.\"\"\"\n        node = self.flat_index.get(component_id)\n        if not node:\n            return None\n\n        # Create updated component\n        component_data = node.component.model_dump()\n        component_data.update(updates)\n        component_data[\"lifecycle\"] = ComponentLifecycle.UPDATE\n        component_data[\"timestamp\"] = datetime.utcnow().isoformat()\n\n        updated_component = node.component.__class__(**component_data)\n        node.component = updated_component\n\n        return ComponentUpdate(\n            operation=UpdateOperation.UPDATE,\n            target_id=component_id,\n            component=updated_component,\n            updates=updates,\n        )\n\n    def replace_component(\n        self, old_id: str, new_component: RichComponent\n    ) -> Optional[ComponentUpdate]:\n        \"\"\"Replace one component with another.\"\"\"\n        old_node = self.flat_index.get(old_id)\n        if not old_node:\n            return None\n\n        # Update the component in place\n        old_node.component = new_component\n\n        # Update index\n        del self.flat_index[old_id]\n        self.flat_index[new_component.id] = old_node\n\n        return ComponentUpdate(\n            operation=UpdateOperation.REPLACE, target_id=old_id, component=new_component\n        )\n\n    def remove_component(self, component_id: str) -> Optional[ComponentUpdate]:\n        \"\"\"Remove a component and its children.\"\"\"\n        node = self.flat_index.get(component_id)\n        if not node:\n            return None\n\n        # Remove from parent\n        if self.root and self.root.component.id == component_id:\n            self.root = None\n        else:\n            if self.root:\n                self.root.remove_child(component_id)\n\n        # Remove from flat index (including all children)\n        removed_ids = node.get_all_ids()\n        for removed_id in removed_ids:\n            self.flat_index.pop(removed_id, None)\n\n        return ComponentUpdate(operation=UpdateOperation.REMOVE, target_id=component_id)\n\n    def get_component(self, component_id: str) -> Optional[RichComponent]:\n        \"\"\"Get a component by ID.\"\"\"\n        node = self.flat_index.get(component_id)\n        return node.component if node else None\n\n    def _find_parent(self, position: Optional[Position]) -> Optional[ComponentNode]:\n        \"\"\"Find the parent node for a new component.\"\"\"\n        if not position or not position.anchor_id:\n            return self.root\n\n        anchor_node = self.flat_index.get(position.anchor_id)\n        if not anchor_node:\n            return self.root\n\n        if position.relation == \"inside\":\n            return anchor_node\n        elif position.relation in [\"before\", \"after\", \"replace\"]:\n            # Find the parent of the anchor\n            if anchor_node.parent_id:\n                parent_node = self.flat_index.get(anchor_node.parent_id)\n                return parent_node if parent_node else self.root\n            else:\n                return self.root\n        else:\n            return self.root\n\n\nclass ComponentManager:\n    \"\"\"Manages component lifecycle and state updates.\"\"\"\n\n    def __init__(self) -> None:\n        self.components: Dict[str, RichComponent] = {}\n        self.component_tree = ComponentTree()\n        self.update_history: List[ComponentUpdate] = []\n        self.active_batch: Optional[str] = None\n\n    def emit(self, component: RichComponent) -> Optional[ComponentUpdate]:\n        \"\"\"Emit a component with smart lifecycle management.\"\"\"\n        if component.id in self.components:\n            # Existing component - determine if this is an update or replace\n            existing = self.components[component.id]\n\n            if component.lifecycle == ComponentLifecycle.UPDATE:\n                # Extract changes\n                old_data = existing.model_dump()\n                new_data = component.model_dump()\n                updates = {k: v for k, v in new_data.items() if old_data.get(k) != v}\n\n                update = self.component_tree.update_component(component.id, updates)\n            else:\n                # Replace\n                update = self.component_tree.replace_component(component.id, component)\n        else:\n            # New component - always append\n            update = self.component_tree.add_component(component, None)\n\n        if update:\n            self.components[component.id] = component\n            self.update_history.append(update)\n\n            if self.active_batch:\n                update.batch_id = self.active_batch\n\n        return update\n\n    def update_component(\n        self, component_id: str, **updates: Any\n    ) -> Optional[ComponentUpdate]:\n        \"\"\"Update specific fields of an existing component.\"\"\"\n        update = self.component_tree.update_component(component_id, updates)\n        if update and update.component:\n            self.components[component_id] = update.component\n            self.update_history.append(update)\n\n            if self.active_batch:\n                update.batch_id = self.active_batch\n\n        return update\n\n    def replace_component(\n        self, old_id: str, new_component: RichComponent\n    ) -> Optional[ComponentUpdate]:\n        \"\"\"Replace one component with another.\"\"\"\n        update = self.component_tree.replace_component(old_id, new_component)\n        if update:\n            self.components.pop(old_id, None)\n            self.components[new_component.id] = new_component\n            self.update_history.append(update)\n\n            if self.active_batch:\n                update.batch_id = self.active_batch\n\n        return update\n\n    def remove_component(self, component_id: str) -> Optional[ComponentUpdate]:\n        \"\"\"Remove a component and handle cleanup.\"\"\"\n        update = self.component_tree.remove_component(component_id)\n        if update:\n            self.components.pop(component_id, None)\n            self.update_history.append(update)\n\n            if self.active_batch:\n                update.batch_id = self.active_batch\n\n        return update\n\n    def get_component(self, component_id: str) -> Optional[RichComponent]:\n        \"\"\"Get a component by ID.\"\"\"\n        return self.components.get(component_id)\n\n    def get_all_components(self) -> List[RichComponent]:\n        \"\"\"Get all components in the manager.\"\"\"\n        return list(self.components.values())\n\n    def start_batch(self) -> str:\n        \"\"\"Start a batch of related updates.\"\"\"\n        self.active_batch = str(uuid.uuid4())\n        return self.active_batch\n\n    def end_batch(self) -> Optional[str]:\n        \"\"\"End the current batch.\"\"\"\n        batch_id = self.active_batch\n        self.active_batch = None\n        return batch_id\n\n    def get_updates_since(\n        self, timestamp: Optional[str] = None\n    ) -> List[ComponentUpdate]:\n        \"\"\"Get all updates since a given timestamp.\"\"\"\n        if not timestamp:\n            return self.update_history.copy()\n\n        try:\n            cutoff = datetime.fromisoformat(timestamp.replace(\"Z\", \"+00:00\"))\n            return [\n                update\n                for update in self.update_history\n                if datetime.fromisoformat(update.timestamp.replace(\"Z\", \"+00:00\"))\n                > cutoff\n            ]\n        except ValueError:\n            return self.update_history.copy()\n\n    def clear_history(self) -> None:\n        \"\"\"Clear the update history.\"\"\"\n        self.update_history.clear()\n"
  },
  {
    "path": "src/vanna/core/components.py",
    "content": "\"\"\"\nUI component base class.\n\nThis module defines the UiComponent class which is the return type for tool executions.\nIt's placed in core/ because it's a fundamental type that tools return, not just a UI concern.\n\"\"\"\n\nfrom datetime import datetime\nfrom typing import Any, Optional\n\nfrom pydantic import BaseModel, Field, model_validator\n\n\nclass UiComponent(BaseModel):\n    \"\"\"Base class for UI components streamed to client.\n\n    This wraps both rich and simple component representations,\n    allowing tools to return structured UI updates.\n\n    Note: We use Any for component types to avoid circular dependencies.\n    Type validation happens at runtime through validators.\n    \"\"\"\n\n    timestamp: str = Field(default_factory=lambda: datetime.utcnow().isoformat())\n    rich_component: Any = Field(\n        ..., description=\"Rich component for advanced rendering\"\n    )\n    simple_component: Optional[Any] = Field(\n        None, description=\"Simple component for basic rendering\"\n    )\n\n    @model_validator(mode=\"after\")\n    def validate_components(self) -> \"UiComponent\":\n        \"\"\"Validate that components are the correct types at runtime.\"\"\"\n        # Import from core - clean imports, no circular dependency\n        from .rich_component import RichComponent\n        from .simple_component import SimpleComponent\n\n        if not isinstance(self.rich_component, RichComponent):\n            raise ValueError(\n                f\"rich_component must be a RichComponent, got {type(self.rich_component)}\"\n            )\n\n        if self.simple_component is not None and not isinstance(\n            self.simple_component, SimpleComponent\n        ):\n            raise ValueError(\n                f\"simple_component must be a SimpleComponent or None, got {type(self.simple_component)}\"\n            )\n\n        return self\n\n    model_config = {\"arbitrary_types_allowed\": True}\n"
  },
  {
    "path": "src/vanna/core/enhancer/__init__.py",
    "content": "\"\"\"\nLLM context enhancement system for adding context to prompts and messages.\n\nThis module provides interfaces for enriching LLM system prompts and messages\nwith additional context before LLM calls (e.g., from memory, RAG, documentation).\n\"\"\"\n\nfrom .base import LlmContextEnhancer\nfrom .default import DefaultLlmContextEnhancer\n\n__all__ = [\"LlmContextEnhancer\", \"DefaultLlmContextEnhancer\"]\n"
  },
  {
    "path": "src/vanna/core/enhancer/base.py",
    "content": "\"\"\"\nLLM context enhancer interface.\n\nLLM context enhancers allow you to add additional context to the system prompt\nand user messages before LLM calls.\n\"\"\"\n\nfrom abc import ABC\nfrom typing import TYPE_CHECKING, Optional\n\nif TYPE_CHECKING:\n    from ..user.models import User\n    from ..llm.models import LlmMessage\n\n\nclass LlmContextEnhancer(ABC):\n    \"\"\"Enhancer for adding context to LLM prompts and messages.\n\n    Subclass this to create custom enhancers that can:\n    - Add relevant context to the system prompt based on the user's initial message\n    - Enrich user messages with additional context (e.g., from memory/RAG)\n    - Inject relevant examples or documentation\n    - Add temporal or environmental context\n\n    Example:\n        class MemoryBasedEnhancer(LlmContextEnhancer):\n            def __init__(self, agent_memory):\n                self.agent_memory = agent_memory\n\n            async def enhance_system_prompt(\n                self,\n                system_prompt: str,\n                user_message: str,\n                user: User\n            ) -> str:\n                # Add relevant examples from memory based on user message\n                examples = await self.agent_memory.search_similar(user_message)\n                return system_prompt + \"\\\\n\\\\nRelevant examples:\\\\n\" + examples\n\n            async def enhance_user_messages(\n                self,\n                messages: list[LlmMessage],\n                user: User\n            ) -> list[LlmMessage]:\n                # Could modify or add to messages\n                return messages\n\n        agent = Agent(\n            llm_service=...,\n            llm_context_enhancer=MemoryBasedEnhancer(agent_memory)\n        )\n    \"\"\"\n\n    async def enhance_system_prompt(\n        self, system_prompt: str, user_message: str, user: \"User\"\n    ) -> str:\n        \"\"\"Enhance the system prompt with additional context.\n\n        This method is called before the first LLM request with the initial\n        user message, allowing you to add relevant context to the system prompt.\n\n        Args:\n            system_prompt: The original system prompt\n            user_message: The initial user message\n            user: The user making the request\n\n        Returns:\n            Enhanced system prompt with additional context\n\n        Note:\n            This is called once per conversation turn, before any tool calls.\n        \"\"\"\n        return system_prompt\n\n    async def enhance_user_messages(\n        self, messages: list[\"LlmMessage\"], user: \"User\"\n    ) -> list[\"LlmMessage\"]:\n        \"\"\"Enhance user messages with additional context.\n\n        This method is called to potentially modify or add context to user messages\n        before sending them to the LLM.\n\n        Args:\n            messages: The list of messages to enhance\n            user: The user making the request\n\n        Returns:\n            Enhanced list of messages\n\n        Note:\n            This is called before each LLM request, including after tool calls.\n            Be careful not to add context repeatedly on each iteration.\n        \"\"\"\n        return messages\n"
  },
  {
    "path": "src/vanna/core/enhancer/default.py",
    "content": "\"\"\"\nDefault LLM context enhancer implementation using AgentMemory.\n\nThis implementation enriches the system prompt with relevant memories\nbased on the user's initial message.\n\"\"\"\n\nfrom typing import TYPE_CHECKING, List, Optional\nfrom .base import LlmContextEnhancer\n\nif TYPE_CHECKING:\n    from ..user.models import User\n    from ..llm.models import LlmMessage\n    from ...capabilities.agent_memory import AgentMemory, TextMemorySearchResult\n\n\nclass DefaultLlmContextEnhancer(LlmContextEnhancer):\n    \"\"\"Default enhancer that uses AgentMemory to add relevant context.\n\n    This enhancer searches the agent's memory for relevant examples and\n    tool use patterns based on the user's message, and adds them to the\n    system prompt.\n\n    Example:\n        agent = Agent(\n            llm_service=...,\n            agent_memory=agent_memory,\n            llm_context_enhancer=DefaultLlmContextEnhancer(agent_memory)\n        )\n    \"\"\"\n\n    def __init__(self, agent_memory: Optional[\"AgentMemory\"] = None):\n        \"\"\"Initialize with optional agent memory.\n\n        Args:\n            agent_memory: Optional AgentMemory instance. If not provided,\n                         enhancement will be skipped.\n        \"\"\"\n        self.agent_memory = agent_memory\n\n    async def enhance_system_prompt(\n        self, system_prompt: str, user_message: str, user: \"User\"\n    ) -> str:\n        \"\"\"Enhance system prompt with relevant memories.\n\n        Searches agent memory for relevant text memories based on the\n        user's message and adds them to the system prompt.\n\n        Args:\n            system_prompt: The original system prompt\n            user_message: The initial user message\n            user: The user making the request\n\n        Returns:\n            Enhanced system prompt with relevant examples from memory\n        \"\"\"\n        if not self.agent_memory:\n            return system_prompt\n\n        try:\n            # Import here to avoid circular dependency\n            from ..tool import ToolContext\n            import uuid\n\n            # Create a temporary context for memory search\n            context = ToolContext(\n                user=user,\n                conversation_id=\"temp\",\n                request_id=str(uuid.uuid4()),\n                agent_memory=self.agent_memory,\n            )\n\n            # Search for relevant text memories based on user message\n            memories: List[\n                \"TextMemorySearchResult\"\n            ] = await self.agent_memory.search_text_memories(\n                query=user_message, context=context, limit=5\n            )\n\n            if not memories:\n                return system_prompt\n\n            # Format memories as context snippets to add to system prompt\n            examples_section = \"\\n\\n## Relevant Context from Memory\\n\\n\"\n            examples_section += \"The following domain knowledge and context from prior interactions may be relevant:\\n\\n\"\n\n            for result in memories:\n                memory = result.memory\n                examples_section += f\"• {memory.content}\\n\"\n\n            # Append examples to system prompt\n            return system_prompt + examples_section\n\n        except Exception as e:\n            # If memory search fails, return original prompt\n            # Don't fail the entire request due to memory issues\n            import logging\n\n            logger = logging.getLogger(__name__)\n            logger.warning(f\"Failed to enhance system prompt with memories: {e}\")\n            return system_prompt\n\n    async def enhance_user_messages(\n        self, messages: list[\"LlmMessage\"], user: \"User\"\n    ) -> list[\"LlmMessage\"]:\n        \"\"\"Enhance user messages.\n\n        The default implementation doesn't modify user messages.\n        Override this to add context to user messages if needed.\n\n        Args:\n            messages: The list of messages\n            user: The user making the request\n\n        Returns:\n            Original list of messages (unmodified)\n        \"\"\"\n        return messages\n"
  },
  {
    "path": "src/vanna/core/enricher/__init__.py",
    "content": "\"\"\"\nContext enrichment system for adding data to tool execution context.\n\nThis module provides interfaces for enriching ToolContext with additional\ndata before tool execution.\n\"\"\"\n\nfrom .base import ToolContextEnricher\n\n__all__ = [\"ToolContextEnricher\"]\n"
  },
  {
    "path": "src/vanna/core/enricher/base.py",
    "content": "\"\"\"\nBase context enricher interface.\n\nContext enrichers allow you to add additional data to the ToolContext\nbefore tools are executed.\n\"\"\"\n\nfrom abc import ABC\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from ..tool.models import ToolContext\n\n\nclass ToolContextEnricher(ABC):\n    \"\"\"Enricher for adding data to ToolContext.\n\n    Subclass this to create custom enrichers that can:\n    - Add user preferences from database\n    - Inject session state\n    - Add temporal context (timezone, current date)\n    - Include user history or profile data\n    - Add environment-specific configuration\n\n    Example:\n        class UserPreferencesEnricher(ToolContextEnricher):\n            def __init__(self, db):\n                self.db = db\n\n            async def enrich_context(self, context: ToolContext) -> ToolContext:\n                # Fetch user preferences\n                prefs = await self.db.get_user_preferences(context.user.id)\n\n                # Add to context metadata\n                context.metadata[\"preferences\"] = prefs\n                context.metadata[\"timezone\"] = prefs.get(\"timezone\", \"UTC\")\n\n                return context\n\n        agent = AgentRunner(\n            llm_service=...,\n            context_enrichers=[UserPreferencesEnricher(db), SessionEnricher()]\n        )\n    \"\"\"\n\n    async def enrich_context(self, context: \"ToolContext\") -> \"ToolContext\":\n        \"\"\"Enrich the tool execution context with additional data.\n\n        Args:\n            context: The tool context to enrich\n\n        Returns:\n            Enriched context (typically modified in-place)\n\n        Note:\n            Enrichers typically modify the context.metadata dict to add\n            additional data that tools can access.\n        \"\"\"\n        return context\n"
  },
  {
    "path": "src/vanna/core/errors.py",
    "content": "\"\"\"\nException classes for the Vanna Agents framework.\n\nThis module defines all custom exceptions used throughout the framework.\n\"\"\"\n\n\nclass AgentError(Exception):\n    \"\"\"Base exception for agent framework.\"\"\"\n\n    pass\n\n\nclass ToolExecutionError(AgentError):\n    \"\"\"Error during tool execution.\"\"\"\n\n    pass\n\n\nclass ToolNotFoundError(AgentError):\n    \"\"\"Tool not found in registry.\"\"\"\n\n    pass\n\n\nclass PermissionError(AgentError):\n    \"\"\"User lacks required permissions.\"\"\"\n\n    pass\n\n\nclass ConversationNotFoundError(AgentError):\n    \"\"\"Conversation not found.\"\"\"\n\n    pass\n\n\nclass LlmServiceError(AgentError):\n    \"\"\"Error communicating with LLM service.\"\"\"\n\n    pass\n\n\nclass ValidationError(AgentError):\n    \"\"\"Data validation error.\"\"\"\n\n    pass\n"
  },
  {
    "path": "src/vanna/core/evaluation/__init__.py",
    "content": "\"\"\"\nEvaluation framework for Vanna Agents.\n\nThis module provides a complete evaluation system for testing and comparing\nagent variants, with special focus on LLM comparison use cases.\n\nKey Features:\n- Parallel execution for efficient I/O-bound operations\n- Multiple built-in evaluators (trajectory, output, LLM-as-judge, efficiency)\n- Rich reporting (HTML, CSV, console)\n- Dataset loaders (YAML, JSON)\n- Agent variant comparison\n\nExample:\n    >>> from vanna.evaluation import (\n    ...     EvaluationRunner,\n    ...     EvaluationDataset,\n    ...     AgentVariant,\n    ...     TrajectoryEvaluator,\n    ...     OutputEvaluator,\n    ... )\n    >>>\n    >>> # Load test dataset\n    >>> dataset = EvaluationDataset.from_yaml(\"tests/sql_tasks.yaml\")\n    >>>\n    >>> # Create agent variants\n    >>> variants = [\n    ...     AgentVariant(\"claude\", claude_agent),\n    ...     AgentVariant(\"gpt\", gpt_agent),\n    ... ]\n    >>>\n    >>> # Run comparison\n    >>> runner = EvaluationRunner(\n    ...     evaluators=[TrajectoryEvaluator(), OutputEvaluator()],\n    ...     max_concurrency=20\n    ... )\n    >>> comparison = await runner.compare_agents(variants, dataset.test_cases)\n    >>> comparison.print_summary()\n\"\"\"\n\nfrom .base import (\n    Evaluator,\n    TestCase,\n    ExpectedOutcome,\n    AgentResult,\n    EvaluationResult,\n    TestCaseResult,\n    AgentVariant,\n)\nfrom .runner import EvaluationRunner\nfrom .evaluators import (\n    TrajectoryEvaluator,\n    OutputEvaluator,\n    LLMAsJudgeEvaluator,\n    EfficiencyEvaluator,\n)\nfrom .report import EvaluationReport, ComparisonReport\nfrom .dataset import EvaluationDataset\n\n__all__ = [\n    # Base classes\n    \"Evaluator\",\n    \"TestCase\",\n    \"ExpectedOutcome\",\n    \"AgentResult\",\n    \"EvaluationResult\",\n    \"TestCaseResult\",\n    \"AgentVariant\",\n    # Runner\n    \"EvaluationRunner\",\n    # Built-in evaluators\n    \"TrajectoryEvaluator\",\n    \"OutputEvaluator\",\n    \"LLMAsJudgeEvaluator\",\n    \"EfficiencyEvaluator\",\n    # Reporting\n    \"EvaluationReport\",\n    \"ComparisonReport\",\n    # Datasets\n    \"EvaluationDataset\",\n]\n"
  },
  {
    "path": "src/vanna/core/evaluation/base.py",
    "content": "\"\"\"\nCore evaluation abstractions for the Vanna Agents framework.\n\nThis module provides the base classes and models for evaluating agent behavior,\nincluding test cases, expected outcomes, and evaluation results.\n\"\"\"\n\nfrom abc import ABC, abstractmethod\nfrom typing import Any, Dict, List, Optional, Callable\nfrom dataclasses import dataclass, field\nfrom datetime import datetime\nfrom pydantic import BaseModel\n\nfrom vanna.core import User, UiComponent\n\n\nclass ExpectedOutcome(BaseModel):\n    \"\"\"Defines what we expect from the agent for a test case.\n\n    Provides multiple ways to specify expectations:\n    - tools_called: List of tool names that should be called\n    - tools_not_called: List of tool names that should NOT be called\n    - final_answer_contains: Keywords/phrases that should appear in output\n    - final_answer_not_contains: Keywords/phrases that should NOT appear\n    - min_components: Minimum number of UI components expected\n    - max_execution_time_ms: Maximum allowed execution time\n    - custom_validators: Custom validation functions\n    \"\"\"\n\n    tools_called: Optional[List[str]] = None\n    tools_not_called: Optional[List[str]] = None\n    final_answer_contains: Optional[List[str]] = None\n    final_answer_not_contains: Optional[List[str]] = None\n    min_components: Optional[int] = None\n    max_components: Optional[int] = None\n    max_execution_time_ms: Optional[float] = None\n    metadata: Dict[str, Any] = {}\n\n\nclass TestCase(BaseModel):\n    \"\"\"A single evaluation test case.\n\n    Attributes:\n        id: Unique identifier for the test case\n        user: User context for the test\n        message: The message to send to the agent\n        conversation_id: Optional conversation ID for multi-turn tests\n        expected_outcome: What we expect the agent to do/produce\n        metadata: Additional metadata for categorization/filtering\n    \"\"\"\n\n    id: str\n    user: User\n    message: str\n    conversation_id: Optional[str] = None\n    expected_outcome: Optional[ExpectedOutcome] = None\n    metadata: Dict[str, Any] = {}\n\n\n@dataclass\nclass AgentResult:\n    \"\"\"The result of running an agent on a test case.\n\n    Captures everything that happened during agent execution\n    for later evaluation.\n    \"\"\"\n\n    test_case_id: str\n    components: List[UiComponent]\n    tool_calls: List[Dict[str, Any]] = field(default_factory=list)\n    llm_requests: List[Dict[str, Any]] = field(default_factory=list)\n    execution_time_ms: float = 0.0\n    total_tokens: int = 0\n    error: Optional[str] = None\n    metadata: Dict[str, Any] = field(default_factory=dict)\n\n    def get_final_answer(self) -> str:\n        \"\"\"Extract the final answer from components.\"\"\"\n        # Find text components and concatenate\n        texts = []\n        for component in self.components:\n            if hasattr(component, \"rich_component\"):\n                rich_comp = component.rich_component\n                if hasattr(rich_comp, \"type\") and rich_comp.type.value == \"text\":\n                    content = rich_comp.data.get(\"content\") or getattr(\n                        rich_comp, \"content\", \"\"\n                    )\n                    if content:\n                        texts.append(content)\n        return \"\\n\".join(texts)\n\n    def get_tool_names_called(self) -> List[str]:\n        \"\"\"Get list of tool names that were called.\"\"\"\n        return [call.get(\"tool_name\", \"\") for call in self.tool_calls]\n\n\nclass EvaluationResult(BaseModel):\n    \"\"\"Result of evaluating a single test case.\n\n    Attributes:\n        test_case_id: ID of the test case evaluated\n        evaluator_name: Name of the evaluator that produced this result\n        passed: Whether the test case passed\n        score: Score from 0.0 to 1.0\n        reasoning: Explanation of the evaluation\n        metrics: Additional metrics captured during evaluation\n        timestamp: When the evaluation was performed\n    \"\"\"\n\n    test_case_id: str\n    evaluator_name: str\n    passed: bool\n    score: float  # 0.0 to 1.0\n    reasoning: str\n    metrics: Dict[str, Any] = {}\n    timestamp: datetime = datetime.now()\n\n\n@dataclass\nclass TestCaseResult:\n    \"\"\"Complete result for a single test case including all evaluations.\"\"\"\n\n    test_case: TestCase\n    agent_result: AgentResult\n    evaluations: List[EvaluationResult]\n    execution_time_ms: float\n\n    def overall_passed(self) -> bool:\n        \"\"\"Check if all evaluations passed.\"\"\"\n        return all(e.passed for e in self.evaluations)\n\n    def overall_score(self) -> float:\n        \"\"\"Calculate average score across all evaluations.\"\"\"\n        if not self.evaluations:\n            return 0.0\n        return sum(e.score for e in self.evaluations) / len(self.evaluations)\n\n\n@dataclass\nclass AgentVariant:\n    \"\"\"A variant of an agent to evaluate (different LLM, config, etc).\n\n    Used for comparing different agent configurations, especially\n    different LLMs or model versions.\n\n    Attributes:\n        name: Human-readable name for this variant\n        agent: The agent instance to evaluate\n        metadata: Additional info (model name, provider, config, etc)\n    \"\"\"\n\n    name: str\n    agent: Any  # Agent type - avoiding circular import\n    metadata: Dict[str, Any] = field(default_factory=dict)\n\n\nclass Evaluator(ABC):\n    \"\"\"Base class for evaluating agent behavior.\n\n    Evaluators examine the agent's execution and determine if it\n    met expectations. Multiple evaluators can be composed to check\n    different aspects (trajectory, output quality, efficiency, etc).\n    \"\"\"\n\n    @property\n    @abstractmethod\n    def name(self) -> str:\n        \"\"\"Name of this evaluator.\"\"\"\n        pass\n\n    @abstractmethod\n    async def evaluate(\n        self,\n        test_case: TestCase,\n        agent_result: AgentResult,\n    ) -> EvaluationResult:\n        \"\"\"Evaluate a single test case execution.\n\n        Args:\n            test_case: The test case that was executed\n            agent_result: The result from running the agent\n\n        Returns:\n            EvaluationResult with pass/fail, score, and reasoning\n        \"\"\"\n        pass\n"
  },
  {
    "path": "src/vanna/core/evaluation/dataset.py",
    "content": "\"\"\"\nDataset loaders for evaluation test cases.\n\nThis module provides utilities for loading test case datasets from\nYAML and JSON files.\n\"\"\"\n\nimport json\nimport yaml\nfrom typing import Any, Dict, List\nfrom pathlib import Path\n\nfrom .base import TestCase, ExpectedOutcome\nfrom vanna.core import User\n\n\nclass EvaluationDataset:\n    \"\"\"Collection of test cases with metadata.\n\n    Example YAML format:\n        dataset:\n          name: \"SQL Generation Tasks\"\n          description: \"Test cases for SQL generation\"\n          test_cases:\n            - id: \"sql_001\"\n              user_id: \"test_user\"\n              message: \"Show me total sales by region\"\n              expected_outcome:\n                tools_called: [\"generate_sql\", \"execute_query\"]\n                final_answer_contains: [\"SELECT\", \"GROUP BY\", \"region\"]\n    \"\"\"\n\n    def __init__(self, name: str, test_cases: List[TestCase], description: str = \"\"):\n        \"\"\"Initialize evaluation dataset.\n\n        Args:\n            name: Name of the dataset\n            test_cases: List of test cases\n            description: Optional description\n        \"\"\"\n        self.name = name\n        self.test_cases = test_cases\n        self.description = description\n\n    @classmethod\n    def from_yaml(cls, path: str) -> \"EvaluationDataset\":\n        \"\"\"Load dataset from YAML file.\n\n        Args:\n            path: Path to YAML file\n\n        Returns:\n            EvaluationDataset instance\n        \"\"\"\n        with open(path, \"r\") as f:\n            data = yaml.safe_load(f)\n\n        return cls._from_dict(data)\n\n    @classmethod\n    def from_json(cls, path: str) -> \"EvaluationDataset\":\n        \"\"\"Load dataset from JSON file.\n\n        Args:\n            path: Path to JSON file\n\n        Returns:\n            EvaluationDataset instance\n        \"\"\"\n        with open(path, \"r\") as f:\n            data = json.load(f)\n\n        return cls._from_dict(data)\n\n    @classmethod\n    def _from_dict(cls, data: Dict[str, Any]) -> \"EvaluationDataset\":\n        \"\"\"Create dataset from dictionary.\n\n        Args:\n            data: Dictionary with dataset structure\n\n        Returns:\n            EvaluationDataset instance\n        \"\"\"\n        dataset_config = data.get(\"dataset\", data)\n        name = dataset_config.get(\"name\", \"Unnamed Dataset\")\n        description = dataset_config.get(\"description\", \"\")\n\n        test_cases = []\n        for tc_data in dataset_config.get(\"test_cases\", []):\n            test_case = cls._parse_test_case(tc_data)\n            test_cases.append(test_case)\n\n        return cls(name=name, test_cases=test_cases, description=description)\n\n    @classmethod\n    def _parse_test_case(cls, data: Dict[str, Any]) -> TestCase:\n        \"\"\"Parse a single test case from dictionary.\n\n        Args:\n            data: Test case dictionary\n\n        Returns:\n            TestCase instance\n        \"\"\"\n        # Create user\n        user_id = data.get(\"user_id\", \"test_user\")\n        user = User(\n            id=user_id,\n            username=data.get(\"username\", user_id),\n            email=data.get(\"email\", f\"{user_id}@example.com\"),\n            group_memberships=data.get(\"user_groups\", []),\n        )\n\n        # Parse expected outcome if present\n        expected_outcome = None\n        if \"expected_outcome\" in data:\n            outcome_data = data[\"expected_outcome\"]\n            expected_outcome = ExpectedOutcome(\n                tools_called=outcome_data.get(\"tools_called\"),\n                tools_not_called=outcome_data.get(\"tools_not_called\"),\n                final_answer_contains=outcome_data.get(\"final_answer_contains\"),\n                final_answer_not_contains=outcome_data.get(\"final_answer_not_contains\"),\n                min_components=outcome_data.get(\"min_components\"),\n                max_components=outcome_data.get(\"max_components\"),\n                max_execution_time_ms=outcome_data.get(\"max_execution_time_ms\"),\n                metadata=outcome_data.get(\"metadata\", {}),\n            )\n\n        return TestCase(\n            id=data[\"id\"],\n            user=user,\n            message=data[\"message\"],\n            conversation_id=data.get(\"conversation_id\"),\n            expected_outcome=expected_outcome,\n            metadata=data.get(\"metadata\", {}),\n        )\n\n    def save_yaml(self, path: str) -> None:\n        \"\"\"Save dataset to YAML file.\n\n        Args:\n            path: Path to save YAML file\n        \"\"\"\n        data = self._to_dict()\n        with open(path, \"w\") as f:\n            yaml.dump(data, f, default_flow_style=False, sort_keys=False)\n\n    def save_json(self, path: str) -> None:\n        \"\"\"Save dataset to JSON file.\n\n        Args:\n            path: Path to save JSON file\n        \"\"\"\n        data = self._to_dict()\n        with open(path, \"w\") as f:\n            json.dump(data, f, indent=2)\n\n    def _to_dict(self) -> Dict[str, Any]:\n        \"\"\"Convert dataset to dictionary.\n\n        Returns:\n            Dictionary representation\n        \"\"\"\n        return {\n            \"dataset\": {\n                \"name\": self.name,\n                \"description\": self.description,\n                \"test_cases\": [self._test_case_to_dict(tc) for tc in self.test_cases],\n            }\n        }\n\n    def _test_case_to_dict(self, test_case: TestCase) -> Dict[str, Any]:\n        \"\"\"Convert test case to dictionary.\n\n        Args:\n            test_case: TestCase to convert\n\n        Returns:\n            Dictionary representation\n        \"\"\"\n        data: Dict[str, Any] = {\n            \"id\": test_case.id,\n            \"user_id\": test_case.user.id,\n            \"username\": test_case.user.username,\n            \"email\": test_case.user.email,\n            \"user_groups\": test_case.user.group_memberships,\n            \"message\": test_case.message,\n        }\n\n        if test_case.conversation_id:\n            data[\"conversation_id\"] = test_case.conversation_id\n\n        if test_case.expected_outcome:\n            outcome = test_case.expected_outcome\n            outcome_dict: Dict[str, Any] = {}\n\n            if outcome.tools_called:\n                outcome_dict[\"tools_called\"] = outcome.tools_called\n            if outcome.tools_not_called:\n                outcome_dict[\"tools_not_called\"] = outcome.tools_not_called\n            if outcome.final_answer_contains:\n                outcome_dict[\"final_answer_contains\"] = outcome.final_answer_contains\n            if outcome.final_answer_not_contains:\n                outcome_dict[\"final_answer_not_contains\"] = (\n                    outcome.final_answer_not_contains\n                )\n            if outcome.min_components is not None:\n                outcome_dict[\"min_components\"] = outcome.min_components\n            if outcome.max_components is not None:\n                outcome_dict[\"max_components\"] = outcome.max_components\n            if outcome.max_execution_time_ms is not None:\n                outcome_dict[\"max_execution_time_ms\"] = outcome.max_execution_time_ms\n            if outcome.metadata:\n                outcome_dict[\"metadata\"] = outcome.metadata\n\n            if outcome_dict:\n                data[\"expected_outcome\"] = outcome_dict\n\n        if test_case.metadata:\n            data[\"metadata\"] = test_case.metadata\n\n        return data\n\n    def filter_by_metadata(self, **kwargs: Any) -> \"EvaluationDataset\":\n        \"\"\"Filter test cases by metadata fields.\n\n        Args:\n            **kwargs: Metadata fields to match\n\n        Returns:\n            New EvaluationDataset with filtered test cases\n        \"\"\"\n        filtered = [\n            tc\n            for tc in self.test_cases\n            if all(tc.metadata.get(k) == v for k, v in kwargs.items())\n        ]\n\n        return EvaluationDataset(\n            name=f\"{self.name} (filtered)\",\n            test_cases=filtered,\n            description=f\"Filtered from: {self.description}\",\n        )\n\n    def __len__(self) -> int:\n        \"\"\"Get number of test cases.\"\"\"\n        return len(self.test_cases)\n\n    def __repr__(self) -> str:\n        \"\"\"String representation.\"\"\"\n        return (\n            f\"EvaluationDataset(name='{self.name}', test_cases={len(self.test_cases)})\"\n        )\n"
  },
  {
    "path": "src/vanna/core/evaluation/evaluators.py",
    "content": "\"\"\"\nBuilt-in evaluators for common evaluation tasks.\n\nThis module provides ready-to-use evaluators for:\n- Trajectory evaluation (tools called, order, efficiency)\n- Output evaluation (content matching, quality)\n- LLM-as-judge evaluation (custom criteria)\n- Efficiency evaluation (time, tokens, cost)\n\"\"\"\n\nfrom typing import Dict, Any, Optional\nfrom datetime import datetime\n\nfrom .base import Evaluator, TestCase, AgentResult, EvaluationResult\nfrom vanna.core import LlmService\n\n\nclass TrajectoryEvaluator(Evaluator):\n    \"\"\"Evaluate the path the agent took (tools called, order, etc).\n\n    Checks if the agent called the expected tools and didn't call\n    unexpected ones. Useful for verifying agent reasoning and planning.\n    \"\"\"\n\n    @property\n    def name(self) -> str:\n        return \"trajectory\"\n\n    async def evaluate(\n        self, test_case: TestCase, agent_result: AgentResult\n    ) -> EvaluationResult:\n        \"\"\"Evaluate tool call trajectory.\"\"\"\n        if agent_result.error:\n            return EvaluationResult(\n                test_case_id=test_case.id,\n                evaluator_name=self.name,\n                passed=False,\n                score=0.0,\n                reasoning=f\"Agent execution failed: {agent_result.error}\",\n            )\n\n        expected = test_case.expected_outcome\n        if not expected:\n            return EvaluationResult(\n                test_case_id=test_case.id,\n                evaluator_name=self.name,\n                passed=True,\n                score=1.0,\n                reasoning=\"No expected outcome specified, passing by default\",\n            )\n\n        tools_called = agent_result.get_tool_names_called()\n        issues = []\n        score = 1.0\n\n        # Check expected tools were called\n        if expected.tools_called:\n            for expected_tool in expected.tools_called:\n                if expected_tool not in tools_called:\n                    issues.append(f\"Expected tool '{expected_tool}' was not called\")\n                    score -= 0.5 / len(expected.tools_called)\n\n        # Check unexpected tools were not called\n        if expected.tools_not_called:\n            for unexpected_tool in expected.tools_not_called:\n                if unexpected_tool in tools_called:\n                    issues.append(f\"Unexpected tool '{unexpected_tool}' was called\")\n                    score -= 0.5 / len(expected.tools_not_called)\n\n        score = max(0.0, min(1.0, score))\n        passed = score >= 0.7  # 70% threshold\n\n        reasoning = \"Trajectory evaluation: \"\n        if issues:\n            reasoning += \"; \".join(issues)\n        else:\n            reasoning += \"All expected tools called, no unexpected tools\"\n\n        return EvaluationResult(\n            test_case_id=test_case.id,\n            evaluator_name=self.name,\n            passed=passed,\n            score=score,\n            reasoning=reasoning,\n            metrics={\n                \"tools_called\": tools_called,\n                \"num_tools_called\": len(tools_called),\n                \"issues\": issues,\n            },\n        )\n\n\nclass OutputEvaluator(Evaluator):\n    \"\"\"Evaluate the final output quality.\n\n    Checks if the output contains expected content and doesn't\n    contain forbidden content. Case-insensitive substring matching.\n    \"\"\"\n\n    @property\n    def name(self) -> str:\n        return \"output\"\n\n    async def evaluate(\n        self, test_case: TestCase, agent_result: AgentResult\n    ) -> EvaluationResult:\n        \"\"\"Evaluate output content.\"\"\"\n        if agent_result.error:\n            return EvaluationResult(\n                test_case_id=test_case.id,\n                evaluator_name=self.name,\n                passed=False,\n                score=0.0,\n                reasoning=f\"Agent execution failed: {agent_result.error}\",\n            )\n\n        expected = test_case.expected_outcome\n        if not expected:\n            return EvaluationResult(\n                test_case_id=test_case.id,\n                evaluator_name=self.name,\n                passed=True,\n                score=1.0,\n                reasoning=\"No expected outcome specified, passing by default\",\n            )\n\n        final_answer = agent_result.get_final_answer().lower()\n        issues = []\n        score = 1.0\n\n        # Check expected content is present\n        if expected.final_answer_contains:\n            for expected_content in expected.final_answer_contains:\n                if expected_content.lower() not in final_answer:\n                    issues.append(\n                        f\"Expected content '{expected_content}' not found in output\"\n                    )\n                    score -= 0.5 / len(expected.final_answer_contains)\n\n        # Check forbidden content is absent\n        if expected.final_answer_not_contains:\n            for forbidden_content in expected.final_answer_not_contains:\n                if forbidden_content.lower() in final_answer:\n                    issues.append(\n                        f\"Forbidden content '{forbidden_content}' found in output\"\n                    )\n                    score -= 0.5 / len(expected.final_answer_not_contains)\n\n        score = max(0.0, min(1.0, score))\n        passed = score >= 0.7  # 70% threshold\n\n        reasoning = \"Output evaluation: \"\n        if issues:\n            reasoning += \"; \".join(issues)\n        else:\n            reasoning += \"All expected content present, no forbidden content\"\n\n        return EvaluationResult(\n            test_case_id=test_case.id,\n            evaluator_name=self.name,\n            passed=passed,\n            score=score,\n            reasoning=reasoning,\n            metrics={\n                \"output_length\": len(final_answer),\n                \"issues\": issues,\n            },\n        )\n\n\nclass LLMAsJudgeEvaluator(Evaluator):\n    \"\"\"Use an LLM to judge agent performance based on custom criteria.\n\n    This evaluator uses a separate LLM to assess the quality of the\n    agent's output based on natural language criteria.\n    \"\"\"\n\n    def __init__(self, judge_llm: LlmService, criteria: str):\n        \"\"\"Initialize LLM-as-judge evaluator.\n\n        Args:\n            judge_llm: The LLM service to use for judging\n            criteria: Natural language description of what to evaluate\n        \"\"\"\n        self.judge_llm = judge_llm\n        self.criteria = criteria\n\n    @property\n    def name(self) -> str:\n        return \"llm_judge\"\n\n    async def evaluate(\n        self, test_case: TestCase, agent_result: AgentResult\n    ) -> EvaluationResult:\n        \"\"\"Evaluate using LLM as judge.\"\"\"\n        if agent_result.error:\n            return EvaluationResult(\n                test_case_id=test_case.id,\n                evaluator_name=self.name,\n                passed=False,\n                score=0.0,\n                reasoning=f\"Agent execution failed: {agent_result.error}\",\n            )\n\n        final_answer = agent_result.get_final_answer()\n\n        # Build prompt for judge\n        judge_prompt = f\"\"\"You are evaluating an AI agent's response to a user query.\n\nUser Query: {test_case.message}\n\nAgent's Response:\n{final_answer}\n\nEvaluation Criteria:\n{self.criteria}\n\nPlease evaluate the response and provide:\n1. A score from 0.0 to 1.0 (where 1.0 is perfect)\n2. Whether it passes (score >= 0.7)\n3. Brief reasoning for your evaluation\n\nRespond in this format:\nSCORE: <number>\nPASSED: <yes/no>\nREASONING: <your explanation>\n\"\"\"\n\n        try:\n            # Call judge LLM\n            from vanna.core.llm import LlmRequest, LlmMessage\n\n            request = LlmRequest(\n                user=test_case.user,\n                messages=[LlmMessage(role=\"user\", content=judge_prompt)],\n                temperature=0.0,  # Deterministic judging\n            )\n\n            response = await self.judge_llm.send_request(request)\n            judgment = response.content or \"\"\n\n            # Parse response\n            score = self._parse_score(judgment)\n            passed = self._parse_passed(judgment)\n            reasoning = self._parse_reasoning(judgment)\n\n            return EvaluationResult(\n                test_case_id=test_case.id,\n                evaluator_name=self.name,\n                passed=passed,\n                score=score,\n                reasoning=reasoning,\n                metrics={\"judge_response\": judgment},\n            )\n\n        except Exception as e:\n            return EvaluationResult(\n                test_case_id=test_case.id,\n                evaluator_name=self.name,\n                passed=False,\n                score=0.0,\n                reasoning=f\"LLM judge evaluation failed: {str(e)}\",\n            )\n\n    def _parse_score(self, judgment: str) -> float:\n        \"\"\"Parse score from judge response.\"\"\"\n        try:\n            for line in judgment.split(\"\\n\"):\n                if line.startswith(\"SCORE:\"):\n                    score_str = line.replace(\"SCORE:\", \"\").strip()\n                    return float(score_str)\n        except Exception:\n            pass\n        return 0.5  # Default if parsing fails\n\n    def _parse_passed(self, judgment: str) -> bool:\n        \"\"\"Parse pass/fail from judge response.\"\"\"\n        for line in judgment.split(\"\\n\"):\n            if line.startswith(\"PASSED:\"):\n                passed_str = line.replace(\"PASSED:\", \"\").strip().lower()\n                return passed_str in [\"yes\", \"true\", \"pass\"]\n        return False\n\n    def _parse_reasoning(self, judgment: str) -> str:\n        \"\"\"Parse reasoning from judge response.\"\"\"\n        for line in judgment.split(\"\\n\"):\n            if line.startswith(\"REASONING:\"):\n                return line.replace(\"REASONING:\", \"\").strip()\n        return judgment  # Return full judgment if no reasoning line found\n\n\nclass EfficiencyEvaluator(Evaluator):\n    \"\"\"Evaluate resource usage (time, tokens, cost).\n\n    Checks if the agent completed within acceptable resource limits.\n    \"\"\"\n\n    def __init__(\n        self,\n        max_execution_time_ms: Optional[float] = None,\n        max_tokens: Optional[int] = None,\n        max_cost_usd: Optional[float] = None,\n    ):\n        \"\"\"Initialize efficiency evaluator.\n\n        Args:\n            max_execution_time_ms: Maximum allowed execution time in milliseconds\n            max_tokens: Maximum allowed token usage\n            max_cost_usd: Maximum allowed cost in USD\n        \"\"\"\n        self.max_execution_time_ms = max_execution_time_ms\n        self.max_tokens = max_tokens\n        self.max_cost_usd = max_cost_usd\n\n    @property\n    def name(self) -> str:\n        return \"efficiency\"\n\n    async def evaluate(\n        self, test_case: TestCase, agent_result: AgentResult\n    ) -> EvaluationResult:\n        \"\"\"Evaluate resource efficiency.\"\"\"\n        issues = []\n        score = 1.0\n\n        # Check execution time\n        if self.max_execution_time_ms:\n            if agent_result.execution_time_ms > self.max_execution_time_ms:\n                issues.append(\n                    f\"Execution time {agent_result.execution_time_ms:.0f}ms \"\n                    f\"exceeded limit {self.max_execution_time_ms:.0f}ms\"\n                )\n                score -= 0.33\n\n        # Check token usage\n        if self.max_tokens:\n            if agent_result.total_tokens > self.max_tokens:\n                issues.append(\n                    f\"Token usage {agent_result.total_tokens} exceeded limit {self.max_tokens}\"\n                )\n                score -= 0.33\n\n        # Check cost (would need cost calculation from metadata)\n        # For now, skip cost evaluation\n\n        # Check from expected outcome if specified\n        expected = test_case.expected_outcome\n        if expected and expected.max_execution_time_ms:\n            if agent_result.execution_time_ms > expected.max_execution_time_ms:\n                issues.append(\n                    f\"Execution time {agent_result.execution_time_ms:.0f}ms \"\n                    f\"exceeded test case limit {expected.max_execution_time_ms:.0f}ms\"\n                )\n                score -= 0.34\n\n        score = max(0.0, min(1.0, score))\n        passed = score >= 0.7\n\n        reasoning = \"Efficiency evaluation: \"\n        if issues:\n            reasoning += \"; \".join(issues)\n        else:\n            reasoning += \"Within resource limits\"\n\n        return EvaluationResult(\n            test_case_id=test_case.id,\n            evaluator_name=self.name,\n            passed=passed,\n            score=score,\n            reasoning=reasoning,\n            metrics={\n                \"execution_time_ms\": agent_result.execution_time_ms,\n                \"total_tokens\": agent_result.total_tokens,\n                \"issues\": issues,\n            },\n        )\n"
  },
  {
    "path": "src/vanna/core/evaluation/report.py",
    "content": "\"\"\"\nEvaluation reporting with HTML, CSV, and console output.\n\nThis module provides classes for generating evaluation reports,\nincluding comparison reports for evaluating multiple agent variants.\n\"\"\"\n\nimport csv\nfrom typing import List, Dict, Optional, Any\nfrom dataclasses import dataclass, field\nfrom datetime import datetime\n\nfrom .base import TestCaseResult, AgentVariant, Evaluator, TestCase\n\n\n@dataclass\nclass EvaluationReport:\n    \"\"\"Report for a single agent's evaluation results.\n\n    Attributes:\n        agent_name: Name of the agent evaluated\n        results: List of results for each test case\n        evaluators: List of evaluators used\n        metadata: Additional metadata about the agent/run\n        timestamp: When the evaluation was run\n    \"\"\"\n\n    agent_name: str\n    results: List[TestCaseResult]\n    evaluators: List[Evaluator]\n    metadata: Dict[str, Any] = field(default_factory=dict)\n    timestamp: datetime = field(default_factory=datetime.now)\n\n    def pass_rate(self) -> float:\n        \"\"\"Calculate overall pass rate (0.0 to 1.0).\"\"\"\n        if not self.results:\n            return 0.0\n        passed = sum(1 for r in self.results if r.overall_passed())\n        return passed / len(self.results)\n\n    def average_score(self) -> float:\n        \"\"\"Calculate average score across all test cases.\"\"\"\n        if not self.results:\n            return 0.0\n        return sum(r.overall_score() for r in self.results) / len(self.results)\n\n    def average_time(self) -> float:\n        \"\"\"Calculate average execution time in milliseconds.\"\"\"\n        if not self.results:\n            return 0.0\n        return sum(r.execution_time_ms for r in self.results) / len(self.results)\n\n    def total_tokens(self) -> int:\n        \"\"\"Calculate total tokens used across all test cases.\"\"\"\n        return sum(r.agent_result.total_tokens for r in self.results)\n\n    def get_failures(self) -> List[TestCaseResult]:\n        \"\"\"Get all failed test cases.\"\"\"\n        return [r for r in self.results if not r.overall_passed()]\n\n    def print_summary(self) -> None:\n        \"\"\"Print summary to console.\"\"\"\n        print(f\"\\n{'=' * 80}\")\n        print(f\"EVALUATION REPORT: {self.agent_name}\")\n        print(f\"{'=' * 80}\")\n        print(f\"Timestamp: {self.timestamp.isoformat()}\")\n        print(f\"Test Cases: {len(self.results)}\")\n        print(f\"Pass Rate: {self.pass_rate():.1%}\")\n        print(f\"Average Score: {self.average_score():.2f}\")\n        print(f\"Average Time: {self.average_time():.0f}ms\")\n        print(f\"Total Tokens: {self.total_tokens()}\")\n        print(f\"{'=' * 80}\\n\")\n\n        failures = self.get_failures()\n        if failures:\n            print(f\"FAILURES ({len(failures)}):\")\n            for result in failures:\n                print(f\"\\n  Test Case: {result.test_case.id}\")\n                print(f\"  Message: {result.test_case.message}\")\n                print(f\"  Score: {result.overall_score():.2f}\")\n                for eval_result in result.evaluations:\n                    if not eval_result.passed:\n                        print(\n                            f\"    [{eval_result.evaluator_name}] {eval_result.reasoning}\"\n                        )\n\n\n@dataclass\nclass ComparisonReport:\n    \"\"\"Report comparing multiple agent variants.\n\n    This is the primary report type for LLM comparison use cases.\n\n    Attributes:\n        variants: List of agent variants compared\n        reports: Dict mapping variant name to EvaluationReport\n        test_cases: Test cases used for comparison\n        timestamp: When the comparison was run\n    \"\"\"\n\n    variants: List[AgentVariant]\n    reports: Dict[str, EvaluationReport]\n    test_cases: List[TestCase]\n    timestamp: datetime = field(default_factory=datetime.now)\n\n    def print_summary(self) -> None:\n        \"\"\"Print comparison summary to console.\"\"\"\n        print(\"\\n\" + \"=\" * 80)\n        print(\"AGENT COMPARISON SUMMARY\")\n        print(\"=\" * 80)\n        print(f\"Timestamp: {self.timestamp.isoformat()}\")\n        print(f\"Variants: {len(self.variants)}\")\n        print(f\"Test Cases: {len(self.test_cases)}\")\n\n        # Table of results\n        print(\n            f\"\\n{'Agent':<25} {'Pass Rate':<12} {'Avg Score':<12} {'Avg Time':<12} {'Tokens':<12}\"\n        )\n        print(\"-\" * 80)\n\n        for variant_name, report in self.reports.items():\n            print(\n                f\"{variant_name:<25} \"\n                f\"{report.pass_rate():<12.1%} \"\n                f\"{report.average_score():<12.2f} \"\n                f\"{report.average_time():<12.0f} \"\n                f\"{report.total_tokens():<12,}\"\n            )\n\n        print(\"=\" * 80 + \"\\n\")\n\n    def get_best_variant(self, metric: str = \"score\") -> str:\n        \"\"\"Get the best performing variant by metric.\n\n        Args:\n            metric: Metric to optimize ('score', 'speed', 'pass_rate')\n\n        Returns:\n            Name of the best variant\n        \"\"\"\n        if metric == \"score\":\n            return max(self.reports.items(), key=lambda x: x[1].average_score())[0]\n        elif metric == \"speed\":\n            return min(self.reports.items(), key=lambda x: x[1].average_time())[0]\n        elif metric == \"pass_rate\":\n            return max(self.reports.items(), key=lambda x: x[1].pass_rate())[0]\n        else:\n            raise ValueError(f\"Unknown metric: {metric}\")\n\n    def save_csv(self, path: str) -> None:\n        \"\"\"Save detailed CSV for further analysis.\n\n        Each row represents one test case × one variant combination.\n        \"\"\"\n        with open(path, \"w\", newline=\"\") as f:\n            writer = csv.writer(f)\n\n            # Header\n            writer.writerow(\n                [\n                    \"variant\",\n                    \"test_case_id\",\n                    \"test_message\",\n                    \"passed\",\n                    \"score\",\n                    \"execution_time_ms\",\n                    \"tokens\",\n                    \"error\",\n                    \"evaluator_scores\",\n                ]\n            )\n\n            # Data rows\n            for variant_name, report in self.reports.items():\n                for result in report.results:\n                    evaluator_scores = {\n                        e.evaluator_name: e.score for e in result.evaluations\n                    }\n\n                    writer.writerow(\n                        [\n                            variant_name,\n                            result.test_case.id,\n                            result.test_case.message[:50],  # Truncate\n                            result.overall_passed(),\n                            result.overall_score(),\n                            result.execution_time_ms,\n                            result.agent_result.total_tokens,\n                            result.agent_result.error or \"\",\n                            str(evaluator_scores),\n                        ]\n                    )\n\n    def save_html(self, path: str) -> None:\n        \"\"\"Save interactive HTML comparison report.\n\n        Generates a rich HTML report with:\n        - Summary statistics\n        - Charts comparing variants\n        - Side-by-side test case results\n        \"\"\"\n        html = self._generate_html()\n        with open(path, \"w\") as f:\n            f.write(html)\n\n    def _generate_html(self) -> str:\n        \"\"\"Generate HTML content for report.\"\"\"\n        # Build HTML report\n        html_parts = [\n            \"<!DOCTYPE html>\",\n            \"<html>\",\n            \"<head>\",\n            \"<title>Agent Comparison Report</title>\",\n            \"<style>\",\n            \"body { font-family: Arial, sans-serif; margin: 20px; }\",\n            \"h1 { color: #333; }\",\n            \"table { border-collapse: collapse; width: 100%; margin: 20px 0; }\",\n            \"th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }\",\n            \"th { background-color: #4CAF50; color: white; }\",\n            \"tr:nth-child(even) { background-color: #f2f2f2; }\",\n            \".passed { color: green; font-weight: bold; }\",\n            \".failed { color: red; font-weight: bold; }\",\n            \".best { background-color: #d4edda !important; }\",\n            \"</style>\",\n            \"</head>\",\n            \"<body>\",\n            f\"<h1>Agent Comparison Report</h1>\",\n            f\"<p>Generated: {self.timestamp.isoformat()}</p>\",\n            f\"<p>Variants: {len(self.variants)} | Test Cases: {len(self.test_cases)}</p>\",\n        ]\n\n        # Summary table\n        html_parts.append(\"<h2>Summary</h2>\")\n        html_parts.append(\"<table>\")\n        html_parts.append(\n            \"<tr><th>Agent</th><th>Pass Rate</th><th>Avg Score</th><th>Avg Time (ms)</th><th>Total Tokens</th></tr>\"\n        )\n\n        best_by_score = self.get_best_variant(\"score\")\n\n        for variant_name, report in self.reports.items():\n            row_class = \"best\" if variant_name == best_by_score else \"\"\n            html_parts.append(\n                f\"<tr class='{row_class}'>\"\n                f\"<td>{variant_name}</td>\"\n                f\"<td>{report.pass_rate():.1%}</td>\"\n                f\"<td>{report.average_score():.2f}</td>\"\n                f\"<td>{report.average_time():.0f}</td>\"\n                f\"<td>{report.total_tokens():,}</td>\"\n                f\"</tr>\"\n            )\n\n        html_parts.append(\"</table>\")\n\n        # Test case details\n        html_parts.append(\"<h2>Test Case Details</h2>\")\n\n        for i, test_case in enumerate(self.test_cases):\n            html_parts.append(f\"<h3>Test Case {i + 1}: {test_case.id}</h3>\")\n            html_parts.append(f\"<p><strong>Message:</strong> {test_case.message}</p>\")\n\n            html_parts.append(\"<table>\")\n            html_parts.append(\n                \"<tr><th>Variant</th><th>Result</th><th>Score</th><th>Time (ms)</th></tr>\"\n            )\n\n            for variant_name, report in self.reports.items():\n                result = next(\n                    (r for r in report.results if r.test_case.id == test_case.id), None\n                )\n                if result:\n                    passed_class = \"passed\" if result.overall_passed() else \"failed\"\n                    passed_text = \"PASS\" if result.overall_passed() else \"FAIL\"\n\n                    html_parts.append(\n                        f\"<tr>\"\n                        f\"<td>{variant_name}</td>\"\n                        f\"<td class='{passed_class}'>{passed_text}</td>\"\n                        f\"<td>{result.overall_score():.2f}</td>\"\n                        f\"<td>{result.execution_time_ms:.0f}</td>\"\n                        f\"</tr>\"\n                    )\n\n            html_parts.append(\"</table>\")\n\n        html_parts.append(\"</body>\")\n        html_parts.append(\"</html>\")\n\n        return \"\\n\".join(html_parts)\n"
  },
  {
    "path": "src/vanna/core/evaluation/runner.py",
    "content": "\"\"\"\nEvaluation runner with parallel execution support.\n\nThis module provides the EvaluationRunner class that executes test cases\nagainst agents with configurable parallelism for efficient evaluation,\nespecially when comparing multiple LLMs or model versions.\n\"\"\"\n\nimport asyncio\nfrom typing import Any, List, Dict, Optional, AsyncGenerator, TYPE_CHECKING\nfrom datetime import datetime\n\nfrom .base import (\n    TestCase,\n    AgentResult,\n    TestCaseResult,\n    AgentVariant,\n    Evaluator,\n)\nfrom vanna.core import UiComponent\nfrom vanna.core.user.request_context import RequestContext\nfrom vanna.core.observability import ObservabilityProvider\n\nif TYPE_CHECKING:\n    from vanna import Agent\n    from .report import EvaluationReport, ComparisonReport\n\n\nclass EvaluationRunner:\n    \"\"\"Run evaluations with parallel execution support.\n\n    The primary use case is comparing multiple agent variants (e.g., different LLMs)\n    on the same set of test cases. The runner executes test cases in parallel with\n    configurable concurrency to handle I/O-bound LLM operations efficiently.\n\n    Example:\n        >>> runner = EvaluationRunner(\n        ...     evaluators=[TrajectoryEvaluator(), OutputEvaluator()],\n        ...     max_concurrency=20\n        ... )\n        >>> comparison = await runner.compare_agents(\n        ...     agent_variants=[claude_variant, gpt_variant],\n        ...     test_cases=dataset.test_cases\n        ... )\n    \"\"\"\n\n    def __init__(\n        self,\n        evaluators: List[Evaluator],\n        max_concurrency: int = 10,\n        observability_provider: Optional[ObservabilityProvider] = None,\n    ):\n        \"\"\"Initialize the evaluation runner.\n\n        Args:\n            evaluators: List of evaluators to apply to each test case\n            max_concurrency: Maximum number of concurrent test case executions\n            observability_provider: Optional observability for tracking eval runs\n        \"\"\"\n        self.evaluators = evaluators\n        self.max_concurrency = max_concurrency\n        self.observability = observability_provider\n        self._semaphore = asyncio.Semaphore(max_concurrency)\n\n    async def run_evaluation(\n        self,\n        agent: \"Agent\",\n        test_cases: List[TestCase],\n    ) -> \"EvaluationReport\":\n        \"\"\"Run evaluation on a single agent.\n\n        Args:\n            agent: The agent to evaluate\n            test_cases: List of test cases to run\n\n        Returns:\n            EvaluationReport with results for all test cases\n        \"\"\"\n        from .report import EvaluationReport\n\n        results = await self._run_test_cases_parallel(agent, test_cases)\n        return EvaluationReport(\n            agent_name=\"agent\",\n            results=results,\n            evaluators=self.evaluators,\n            timestamp=datetime.now(),\n        )\n\n    async def compare_agents(\n        self,\n        agent_variants: List[AgentVariant],\n        test_cases: List[TestCase],\n    ) -> \"ComparisonReport\":\n        \"\"\"Compare multiple agent variants on same test cases.\n\n        This is the PRIMARY use case for LLM comparison. Runs all variants\n        in parallel for maximum efficiency with I/O-bound LLM calls.\n\n        Args:\n            agent_variants: List of agent variants to compare\n            test_cases: Test cases to run on each variant\n\n        Returns:\n            ComparisonReport with results for all variants\n        \"\"\"\n        from .report import ComparisonReport\n\n        # Create span for overall comparison\n        if self.observability:\n            span = await self.observability.create_span(\n                \"agent_comparison\",\n                attributes={\n                    \"num_variants\": len(agent_variants),\n                    \"num_test_cases\": len(test_cases),\n                },\n            )\n\n        # Run all variants in parallel\n        tasks = [\n            self._run_agent_variant(variant, test_cases) for variant in agent_variants\n        ]\n\n        variant_reports = await asyncio.gather(*tasks)\n\n        if self.observability:\n            await self.observability.end_span(span)\n\n        return ComparisonReport(\n            variants=agent_variants,\n            reports=dict(zip([v.name for v in agent_variants], variant_reports)),\n            test_cases=test_cases,\n            timestamp=datetime.now(),\n        )\n\n    async def compare_agents_streaming(\n        self,\n        agent_variants: List[AgentVariant],\n        test_cases: List[TestCase],\n    ) -> AsyncGenerator[tuple[str, TestCaseResult, int, int], None]:\n        \"\"\"Stream comparison results as they complete.\n\n        Useful for long-running evaluations where you want to see\n        progress updates in real-time (e.g., for UI display).\n\n        Args:\n            agent_variants: Agent variants to compare\n            test_cases: Test cases to run\n\n        Yields:\n            Tuples of (variant_name, result, completed_count, total_count)\n        \"\"\"\n        queue: asyncio.Queue[tuple[str, TestCaseResult]] = asyncio.Queue()\n\n        async def worker(variant: AgentVariant) -> None:\n            \"\"\"Worker that runs test cases for one variant.\"\"\"\n            results = await self._run_test_cases_parallel(variant.agent, test_cases)\n            for result in results:\n                await queue.put((variant.name, result))\n\n        # Start all workers\n        workers = [asyncio.create_task(worker(v)) for v in agent_variants]\n\n        # Yield results as they arrive\n        completed = 0\n        total = len(agent_variants) * len(test_cases)\n\n        while completed < total:\n            variant_name, result = await queue.get()\n            completed += 1\n            yield variant_name, result, completed, total\n\n        # Wait for all workers to complete\n        await asyncio.gather(*workers)\n\n    async def _run_agent_variant(\n        self,\n        variant: AgentVariant,\n        test_cases: List[TestCase],\n    ) -> \"EvaluationReport\":\n        \"\"\"Run a single agent variant on all test cases.\n\n        Args:\n            variant: The agent variant to evaluate\n            test_cases: Test cases to run\n\n        Returns:\n            EvaluationReport for this variant\n        \"\"\"\n        from .report import EvaluationReport\n\n        if self.observability:\n            span = await self.observability.create_span(\n                f\"variant_{variant.name}\",\n                attributes={\n                    \"variant\": variant.name,\n                    \"num_test_cases\": len(test_cases),\n                    **variant.metadata,\n                },\n            )\n\n        results = await self._run_test_cases_parallel(variant.agent, test_cases)\n\n        if self.observability:\n            await self.observability.end_span(span)\n\n        return EvaluationReport(\n            agent_name=variant.name,\n            results=results,\n            evaluators=self.evaluators,\n            metadata=variant.metadata,\n            timestamp=datetime.now(),\n        )\n\n    async def _run_test_cases_parallel(\n        self,\n        agent: \"Agent\",\n        test_cases: List[TestCase],\n    ) -> List[TestCaseResult]:\n        \"\"\"Run test cases in parallel with concurrency limit.\n\n        Args:\n            agent: The agent to run test cases on\n            test_cases: Test cases to execute\n\n        Returns:\n            List of TestCaseResult, one per test case\n        \"\"\"\n        tasks = [\n            self._run_single_test_case(agent, test_case) for test_case in test_cases\n        ]\n\n        return await asyncio.gather(*tasks)\n\n    async def _run_single_test_case(\n        self,\n        agent: \"Agent\",\n        test_case: TestCase,\n    ) -> TestCaseResult:\n        \"\"\"Run a single test case with semaphore to limit concurrency.\n\n        Args:\n            agent: The agent to execute\n            test_case: The test case to run\n\n        Returns:\n            TestCaseResult with agent execution and evaluations\n        \"\"\"\n        async with self._semaphore:\n            # Execute agent\n            start_time = asyncio.get_event_loop().time()\n            agent_result = await self._execute_agent(agent, test_case)\n            execution_time = asyncio.get_event_loop().time() - start_time\n\n            # Run evaluators\n            eval_results = []\n            for evaluator in self.evaluators:\n                eval_result = await evaluator.evaluate(test_case, agent_result)\n                eval_results.append(eval_result)\n\n            return TestCaseResult(\n                test_case=test_case,\n                agent_result=agent_result,\n                evaluations=eval_results,\n                execution_time_ms=execution_time * 1000,\n            )\n\n    async def _execute_agent(\n        self,\n        agent: \"Agent\",\n        test_case: TestCase,\n    ) -> AgentResult:\n        \"\"\"Execute agent and capture full trajectory.\n\n        Args:\n            agent: The agent to execute\n            test_case: The test case to run\n\n        Returns:\n            AgentResult with all captured data\n        \"\"\"\n        components: List[UiComponent] = []\n        tool_calls: List[Dict[str, Any]] = []\n        error: Optional[str] = None\n\n        try:\n            # Create request context with user info from test case\n            # This allows the agent's UserResolver to resolve the correct user\n            request_context = RequestContext(\n                cookies={\"user_id\": test_case.user.id},\n                headers={},\n                metadata={\"test_case_user\": test_case.user},\n            )\n\n            async for component in agent.send_message(\n                request_context=request_context,\n                message=test_case.message,\n                conversation_id=test_case.conversation_id,\n            ):\n                components.append(component)\n\n        except Exception as e:\n            error = str(e)\n\n        # TODO: Extract tool calls and LLM requests from observability\n        # For now, these will be empty unless we hook into observability\n\n        return AgentResult(\n            test_case_id=test_case.id,\n            components=components,\n            tool_calls=tool_calls,\n            llm_requests=[],\n            error=error,\n        )\n"
  },
  {
    "path": "src/vanna/core/filter/__init__.py",
    "content": "\"\"\"\nConversation filtering system for managing conversation history.\n\nThis module provides interfaces for filtering and transforming conversation\nhistory before it's sent to the LLM.\n\"\"\"\n\nfrom .base import ConversationFilter\n\n__all__ = [\"ConversationFilter\"]\n"
  },
  {
    "path": "src/vanna/core/filter/base.py",
    "content": "\"\"\"\nBase conversation filter interface.\n\nConversation filters allow you to transform conversation history before\nit's sent to the LLM for processing.\n\"\"\"\n\nfrom abc import ABC\nfrom typing import TYPE_CHECKING, List\n\nif TYPE_CHECKING:\n    from ..storage import Message\n\n\nclass ConversationFilter(ABC):\n    \"\"\"Filter for transforming conversation history.\n\n    Subclass this to create custom filters that can:\n    - Remove sensitive information\n    - Summarize long conversations\n    - Manage context window limits\n    - Deduplicate similar messages\n    - Prioritize recent or relevant messages\n\n    Example:\n        class ContextWindowFilter(ConversationFilter):\n            def __init__(self, max_tokens: int = 8000):\n                self.max_tokens = max_tokens\n\n            async def filter_messages(self, messages: List[Message]) -> List[Message]:\n                # Estimate tokens (rough approximation)\n                total_tokens = 0\n                filtered = []\n\n                # Keep system message and recent messages\n                for msg in reversed(messages):\n                    msg_tokens = len(msg.content or \"\") // 4\n                    if total_tokens + msg_tokens > self.max_tokens:\n                        break\n                    filtered.insert(0, msg)\n                    total_tokens += msg_tokens\n\n                return filtered\n\n        agent = AgentRunner(\n            llm_service=...,\n            conversation_filters=[\n                SensitiveDataFilter(),\n                ContextWindowFilter(max_tokens=8000)\n            ]\n        )\n    \"\"\"\n\n    async def filter_messages(self, messages: List[\"Message\"]) -> List[\"Message\"]:\n        \"\"\"Filter and transform conversation messages.\n\n        Args:\n            messages: List of conversation messages\n\n        Returns:\n            Filtered/transformed list of messages\n\n        Note:\n            Filters are applied in order, so messages passed to later\n            filters may already be modified by earlier filters.\n        \"\"\"\n        return messages\n"
  },
  {
    "path": "src/vanna/core/lifecycle/__init__.py",
    "content": "\"\"\"\nLifecycle hook system for agent execution.\n\nThis module provides hooks for intercepting and modifying agent behavior\nat various points in the execution lifecycle.\n\"\"\"\n\nfrom .base import LifecycleHook\n\n__all__ = [\"LifecycleHook\"]\n"
  },
  {
    "path": "src/vanna/core/lifecycle/base.py",
    "content": "\"\"\"\nBase lifecycle hook interface.\n\nLifecycle hooks allow you to intercept and customize agent behavior\nat key points in the execution flow.\n\"\"\"\n\nfrom abc import ABC\nfrom typing import TYPE_CHECKING, Any, Optional\n\nif TYPE_CHECKING:\n    from ..user.models import User\n    from ..tool import Tool\n    from ..tool.models import ToolContext, ToolResult\n\n\nclass LifecycleHook(ABC):\n    \"\"\"Hook into agent execution lifecycle.\n\n    Subclass this to create custom hooks that can:\n    - Modify messages before processing\n    - Add logging or telemetry\n    - Enforce quotas or rate limits\n    - Transform tool results\n    - Add custom validation\n\n    Example:\n        class LoggingHook(LifecycleHook):\n            async def before_message(self, user: User, message: str) -> Optional[str]:\n                print(f\"User {user.username} sent: {message}\")\n                return None  # Don't modify\n\n        agent = AgentRunner(\n            llm_service=...,\n            lifecycle_hooks=[LoggingHook(), QuotaCheckHook()]\n        )\n    \"\"\"\n\n    async def before_message(self, user: \"User\", message: str) -> Optional[str]:\n        \"\"\"Called before processing a user message.\n\n        Args:\n            user: User sending the message\n            message: Original message content\n\n        Returns:\n            Modified message string, or None to keep original\n\n        Raises:\n            AgentError: To halt message processing (e.g., quota exceeded)\n        \"\"\"\n        return None\n\n    async def after_message(self, result: Any) -> None:\n        \"\"\"Called after message has been fully processed.\n\n        Args:\n            result: Final result from message processing\n        \"\"\"\n        pass\n\n    async def before_tool(self, tool: \"Tool[Any]\", context: \"ToolContext\") -> None:\n        \"\"\"Called before tool execution.\n\n        Args:\n            tool: Tool about to be executed\n            context: Tool execution context\n\n        Raises:\n            AgentError: To prevent tool execution\n        \"\"\"\n        pass\n\n    async def after_tool(self, result: \"ToolResult\") -> Optional[\"ToolResult\"]:\n        \"\"\"Called after tool execution.\n\n        Args:\n            result: Result from tool execution\n\n        Returns:\n            Modified ToolResult, or None to keep original\n        \"\"\"\n        return None\n"
  },
  {
    "path": "src/vanna/core/llm/__init__.py",
    "content": "\"\"\"\nLLM domain.\n\nThis module provides the core abstractions for LLM services in the Vanna Agents framework.\n\"\"\"\n\nfrom .base import LlmService\nfrom .models import LlmMessage, LlmRequest, LlmResponse, LlmStreamChunk\n\n__all__ = [\n    \"LlmService\",\n    \"LlmMessage\",\n    \"LlmRequest\",\n    \"LlmResponse\",\n    \"LlmStreamChunk\",\n]\n"
  },
  {
    "path": "src/vanna/core/llm/base.py",
    "content": "\"\"\"\nLLM domain interface.\n\nThis module contains the abstract base class for LLM services.\n\"\"\"\n\nfrom abc import ABC, abstractmethod\nfrom typing import Any, AsyncGenerator, List\n\nfrom .models import LlmRequest, LlmResponse, LlmStreamChunk\n\n\nclass LlmService(ABC):\n    \"\"\"Service for LLM communication.\"\"\"\n\n    @abstractmethod\n    async def send_request(self, request: LlmRequest) -> LlmResponse:\n        \"\"\"Send a request to the LLM.\"\"\"\n        pass\n\n    @abstractmethod\n    async def stream_request(\n        self, request: LlmRequest\n    ) -> AsyncGenerator[LlmStreamChunk, None]:\n        \"\"\"Stream a request to the LLM.\n\n        Args:\n            request: The LLM request to stream\n\n        Yields:\n            LlmStreamChunk instances as they arrive\n        \"\"\"\n        # This is an async generator method\n        raise NotImplementedError\n        yield  # pragma: no cover - makes this an async generator\n\n    @abstractmethod\n    async def validate_tools(self, tools: List[Any]) -> List[str]:\n        \"\"\"Validate tool schemas and return any errors.\"\"\"\n        pass\n"
  },
  {
    "path": "src/vanna/core/llm/models.py",
    "content": "\"\"\"\nLLM domain models.\n\nThis module contains data models for LLM communication.\n\"\"\"\n\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field\n\nfrom ..tool.models import ToolCall\nfrom ..user.models import User\n\n\nclass LlmMessage(BaseModel):\n    \"\"\"Message format for LLM communication.\"\"\"\n\n    role: str = Field(description=\"Message role\")\n    content: str = Field(description=\"Message content\")\n    tool_calls: Optional[List[ToolCall]] = Field(default=None)\n    tool_call_id: Optional[str] = Field(default=None)\n\n\nclass LlmRequest(BaseModel):\n    \"\"\"Request to LLM service.\"\"\"\n\n    messages: List[LlmMessage] = Field(description=\"Messages to send\")\n    tools: Optional[List[Any]] = Field(\n        default=None, description=\"Available tools\"\n    )  # Will be ToolSchema but avoiding circular import\n    user: User = Field(description=\"User making the request\")\n    stream: bool = Field(default=False, description=\"Whether to stream response\")\n    temperature: float = Field(default=0.7, ge=0.0, le=2.0)\n    max_tokens: Optional[int] = Field(default=None, gt=0)\n    system_prompt: Optional[str] = Field(\n        default=None, description=\"System prompt for the LLM\"\n    )\n    metadata: Dict[str, Any] = Field(default_factory=dict)\n\n\nclass LlmResponse(BaseModel):\n    \"\"\"Response from LLM.\"\"\"\n\n    content: Optional[str] = None\n    tool_calls: Optional[List[ToolCall]] = None\n    finish_reason: Optional[str] = None\n    usage: Optional[Dict[str, int]] = None\n    metadata: Dict[str, Any] = Field(default_factory=dict)\n\n    def is_tool_call(self) -> bool:\n        \"\"\"Check if this response contains tool calls.\"\"\"\n        return self.tool_calls is not None and len(self.tool_calls) > 0\n\n\nclass LlmStreamChunk(BaseModel):\n    \"\"\"Streaming chunk from LLM.\"\"\"\n\n    content: Optional[str] = None\n    tool_calls: Optional[List[ToolCall]] = None\n    finish_reason: Optional[str] = None\n    metadata: Dict[str, Any] = Field(default_factory=dict)\n"
  },
  {
    "path": "src/vanna/core/middleware/__init__.py",
    "content": "\"\"\"\nMiddleware system for LLM request/response interception.\n\nThis module provides middleware interfaces for intercepting and transforming\nLLM requests and responses.\n\"\"\"\n\nfrom .base import LlmMiddleware\n\n__all__ = [\"LlmMiddleware\"]\n"
  },
  {
    "path": "src/vanna/core/middleware/base.py",
    "content": "\"\"\"\nBase LLM middleware interface.\n\nMiddleware allows you to intercept and transform LLM requests and responses\nfor caching, monitoring, content filtering, and more.\n\"\"\"\n\nfrom abc import ABC\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from ..llm import LlmRequest, LlmResponse\n\n\nclass LlmMiddleware(ABC):\n    \"\"\"Middleware for intercepting LLM requests and responses.\n\n    Subclass this to create custom middleware that can:\n    - Cache LLM responses\n    - Log requests/responses\n    - Filter or modify content\n    - Track costs and usage\n    - Implement fallback strategies\n\n    Example:\n        class CachingMiddleware(LlmMiddleware):\n            def __init__(self):\n                self.cache = {}\n\n            async def before_llm_request(self, request: LlmRequest) -> LlmRequest:\n                # Could check cache here\n                return request\n\n            async def after_llm_response(self, request: LlmRequest, response: LlmResponse) -> LlmResponse:\n                # Cache the response\n                cache_key = self._compute_key(request)\n                self.cache[cache_key] = response\n                return response\n\n        agent = AgentRunner(\n            llm_service=...,\n            llm_middlewares=[CachingMiddleware(), LoggingMiddleware()]\n        )\n    \"\"\"\n\n    async def before_llm_request(self, request: \"LlmRequest\") -> \"LlmRequest\":\n        \"\"\"Called before sending request to LLM.\n\n        Args:\n            request: The LLM request about to be sent\n\n        Returns:\n            Modified request, or original if no changes\n        \"\"\"\n        return request\n\n    async def after_llm_response(\n        self, request: \"LlmRequest\", response: \"LlmResponse\"\n    ) -> \"LlmResponse\":\n        \"\"\"Called after receiving response from LLM.\n\n        Args:\n            request: The original request\n            response: The LLM response\n\n        Returns:\n            Modified response, or original if no changes\n        \"\"\"\n        return response\n"
  },
  {
    "path": "src/vanna/core/observability/__init__.py",
    "content": "\"\"\"\nObservability system for telemetry and monitoring.\n\nThis module provides interfaces for collecting metrics, traces, and\nmonitoring agent behavior.\n\"\"\"\n\nfrom .base import ObservabilityProvider\nfrom .models import Span, Metric\n\n__all__ = [\"ObservabilityProvider\", \"Span\", \"Metric\"]\n"
  },
  {
    "path": "src/vanna/core/observability/base.py",
    "content": "\"\"\"\nBase observability provider interface.\n\nObservability providers allow you to collect telemetry data about\nagent execution for monitoring and debugging.\n\"\"\"\n\nfrom abc import ABC\nfrom typing import Any, Dict, Optional\n\nfrom .models import Span, Metric\n\n\nclass ObservabilityProvider(ABC):\n    \"\"\"Provider for collecting telemetry and observability data.\n\n    Subclass this to create custom observability integrations that can:\n    - Emit metrics to monitoring systems\n    - Create distributed traces\n    - Log performance data\n    - Track costs and usage\n    - Monitor error rates\n\n    Example:\n        class PrometheusProvider(ObservabilityProvider):\n            def __init__(self, registry):\n                self.registry = registry\n                self.request_counter = Counter(\n                    'agent_requests_total',\n                    'Total agent requests',\n                    registry=registry\n                )\n\n            async def record_metric(self, name: str, value: float, tags: Dict[str, str]) -> None:\n                if name == \"agent.request\":\n                    self.request_counter.inc()\n\n            async def create_span(self, name: str, attributes: Optional[Dict[str, Any]] = None) -> Span:\n                span = Span(name=name, attributes=attributes or {})\n                return span\n\n        agent = AgentRunner(\n            llm_service=...,\n            observability_provider=PrometheusProvider(registry)\n        )\n    \"\"\"\n\n    async def record_metric(\n        self,\n        name: str,\n        value: float,\n        unit: str = \"\",\n        tags: Optional[Dict[str, str]] = None,\n    ) -> None:\n        \"\"\"Record a metric measurement.\n\n        Args:\n            name: Metric name (e.g., \"agent.request.duration\")\n            value: Metric value\n            unit: Unit of measurement (e.g., \"ms\", \"tokens\")\n            tags: Additional tags/labels for the metric\n        \"\"\"\n        pass\n\n    async def create_span(\n        self, name: str, attributes: Optional[Dict[str, Any]] = None\n    ) -> Span:\n        \"\"\"Create a new span for tracing.\n\n        Args:\n            name: Span name/operation\n            attributes: Initial span attributes\n\n        Returns:\n            Span object to track the operation\n\n        Note:\n            Call span.end() when the operation completes.\n        \"\"\"\n        return Span(name=name, attributes=attributes or {})\n\n    async def end_span(self, span: Span) -> None:\n        \"\"\"End a span and record it.\n\n        Args:\n            span: The span to end\n        \"\"\"\n        span.end()\n"
  },
  {
    "path": "src/vanna/core/observability/models.py",
    "content": "\"\"\"\nObservability models for spans and metrics.\n\"\"\"\n\nimport time\nfrom typing import Any, Dict, Optional\nfrom uuid import uuid4\n\nfrom pydantic import BaseModel, Field\n\n\nclass Span(BaseModel):\n    \"\"\"Represents a unit of work for distributed tracing.\"\"\"\n\n    id: str = Field(default_factory=lambda: str(uuid4()), description=\"Span ID\")\n    name: str = Field(description=\"Span name/operation\")\n    start_time: float = Field(default_factory=time.time, description=\"Start timestamp\")\n    end_time: Optional[float] = Field(default=None, description=\"End timestamp\")\n    attributes: Dict[str, Any] = Field(\n        default_factory=dict, description=\"Span attributes\"\n    )\n    parent_id: Optional[str] = Field(default=None, description=\"Parent span ID\")\n\n    def end(self) -> None:\n        \"\"\"Mark span as ended.\"\"\"\n        if self.end_time is None:\n            self.end_time = time.time()\n\n    def duration_ms(self) -> Optional[float]:\n        \"\"\"Get span duration in milliseconds.\"\"\"\n        if self.end_time is None:\n            return None\n        return (self.end_time - self.start_time) * 1000\n\n    def set_attribute(self, key: str, value: Any) -> None:\n        \"\"\"Set a span attribute.\"\"\"\n        self.attributes[key] = value\n\n\nclass Metric(BaseModel):\n    \"\"\"Represents a metric measurement.\"\"\"\n\n    name: str = Field(description=\"Metric name\")\n    value: float = Field(description=\"Metric value\")\n    unit: str = Field(default=\"\", description=\"Unit of measurement\")\n    tags: Dict[str, str] = Field(default_factory=dict, description=\"Metric tags\")\n    timestamp: float = Field(default_factory=time.time, description=\"Measurement time\")\n"
  },
  {
    "path": "src/vanna/core/recovery/__init__.py",
    "content": "\"\"\"\nError recovery system for handling failures gracefully.\n\nThis module provides interfaces for custom error handling, retry logic,\nand fallback strategies.\n\"\"\"\n\nfrom .base import ErrorRecoveryStrategy\nfrom .models import RecoveryAction, RecoveryActionType\n\n__all__ = [\"ErrorRecoveryStrategy\", \"RecoveryAction\", \"RecoveryActionType\"]\n"
  },
  {
    "path": "src/vanna/core/recovery/base.py",
    "content": "\"\"\"\nBase error recovery strategy interface.\n\nRecovery strategies allow you to customize how the agent handles errors\nduring tool execution and LLM communication.\n\"\"\"\n\nfrom abc import ABC\nfrom typing import TYPE_CHECKING\n\nfrom .models import RecoveryAction, RecoveryActionType\n\nif TYPE_CHECKING:\n    from ..tool.models import ToolContext\n    from ..llm import LlmRequest\n\n\nclass ErrorRecoveryStrategy(ABC):\n    \"\"\"Strategy for handling errors and implementing retry logic.\n\n    Subclass this to create custom error recovery strategies that can:\n    - Retry failed operations with backoff\n    - Fallback to alternative approaches\n    - Log errors to external systems\n    - Gracefully degrade functionality\n\n    Example:\n        class ExponentialBackoffStrategy(ErrorRecoveryStrategy):\n            async def handle_tool_error(\n                self, error: Exception, context: ToolContext, attempt: int\n            ) -> RecoveryAction:\n                if attempt < 3:\n                    delay = (2 ** attempt) * 1000  # Exponential backoff\n                    return RecoveryAction(\n                        action=RecoveryActionType.RETRY,\n                        retry_delay_ms=delay,\n                        message=f\"Retrying after {delay}ms\"\n                    )\n                return RecoveryAction(\n                    action=RecoveryActionType.FAIL,\n                    message=\"Max retries exceeded\"\n                )\n\n        agent = AgentRunner(\n            llm_service=...,\n            error_recovery_strategy=ExponentialBackoffStrategy()\n        )\n    \"\"\"\n\n    async def handle_tool_error(\n        self, error: Exception, context: \"ToolContext\", attempt: int = 1\n    ) -> RecoveryAction:\n        \"\"\"Handle errors during tool execution.\n\n        Args:\n            error: The exception that occurred\n            context: Tool execution context\n            attempt: Current attempt number (1-indexed)\n\n        Returns:\n            RecoveryAction indicating how to proceed\n        \"\"\"\n        # Default: fail immediately\n        return RecoveryAction(\n            action=RecoveryActionType.FAIL, message=f\"Tool error: {str(error)}\"\n        )\n\n    async def handle_llm_error(\n        self, error: Exception, request: \"LlmRequest\", attempt: int = 1\n    ) -> RecoveryAction:\n        \"\"\"Handle errors during LLM communication.\n\n        Args:\n            error: The exception that occurred\n            request: The LLM request that failed\n            attempt: Current attempt number (1-indexed)\n\n        Returns:\n            RecoveryAction indicating how to proceed\n        \"\"\"\n        # Default: fail immediately\n        return RecoveryAction(\n            action=RecoveryActionType.FAIL, message=f\"LLM error: {str(error)}\"\n        )\n"
  },
  {
    "path": "src/vanna/core/recovery/models.py",
    "content": "\"\"\"\nRecovery action models for error handling.\n\"\"\"\n\nfrom enum import Enum\nfrom typing import Any, Optional\n\nfrom pydantic import BaseModel, Field\n\n\nclass RecoveryActionType(str, Enum):\n    \"\"\"Types of recovery actions.\"\"\"\n\n    RETRY = \"retry\"\n    FAIL = \"fail\"\n    FALLBACK = \"fallback\"\n    SKIP = \"skip\"\n\n\nclass RecoveryAction(BaseModel):\n    \"\"\"Action to take when recovering from an error.\"\"\"\n\n    action: RecoveryActionType = Field(description=\"Type of recovery action\")\n    retry_delay_ms: Optional[int] = Field(\n        default=None, description=\"Delay before retry in milliseconds\"\n    )\n    fallback_value: Optional[Any] = Field(\n        default=None, description=\"Fallback value to use\"\n    )\n    message: Optional[str] = Field(\n        default=None, description=\"Message to include with action\"\n    )\n"
  },
  {
    "path": "src/vanna/core/registry.py",
    "content": "\"\"\"\nTool registry for the Vanna Agents framework.\n\nThis module provides the ToolRegistry class for managing and executing tools.\n\"\"\"\n\nimport time\nfrom typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, TypeVar, Union\n\nfrom .tool import Tool, ToolCall, ToolContext, ToolRejection, ToolResult, ToolSchema\nfrom .user import User\n\nif TYPE_CHECKING:\n    from .audit import AuditLogger\n    from .agent.config import AuditConfig\n\nT = TypeVar(\"T\")\n\n\nclass _LocalToolWrapper(Tool[T]):\n    \"\"\"Wrapper for tools with configurable access groups.\"\"\"\n\n    def __init__(self, wrapped_tool: Tool[T], access_groups: List[str]):\n        self._wrapped_tool = wrapped_tool\n        self._access_groups = access_groups\n\n    @property\n    def name(self) -> str:\n        return self._wrapped_tool.name\n\n    @property\n    def description(self) -> str:\n        return self._wrapped_tool.description\n\n    @property\n    def access_groups(self) -> List[str]:\n        return self._access_groups\n\n    def get_args_schema(self) -> Type[T]:\n        return self._wrapped_tool.get_args_schema()\n\n    async def execute(self, context: ToolContext, args: T) -> ToolResult:\n        return await self._wrapped_tool.execute(context, args)\n\n\nclass ToolRegistry:\n    \"\"\"Registry for managing tools.\"\"\"\n\n    def __init__(\n        self,\n        audit_logger: Optional[\"AuditLogger\"] = None,\n        audit_config: Optional[\"AuditConfig\"] = None,\n    ) -> None:\n        self._tools: Dict[str, Tool[Any]] = {}\n        self.audit_logger = audit_logger\n        if audit_config is not None:\n            self.audit_config = audit_config\n        else:\n            from .agent.config import AuditConfig\n\n            self.audit_config = AuditConfig()\n\n    def register_local_tool(self, tool: Tool[Any], access_groups: List[str]) -> None:\n        \"\"\"Register a local tool with optional access group restrictions.\n\n        Args:\n            tool: The tool to register\n            access_groups: List of groups that can access this tool.\n                          If None or empty, tool is accessible to all users.\n        \"\"\"\n        if tool.name in self._tools:\n            raise ValueError(f\"Tool '{tool.name}' already registered\")\n\n        if access_groups:\n            # Wrap the tool with access groups\n            wrapped_tool = _LocalToolWrapper(tool, access_groups)\n            self._tools[tool.name] = wrapped_tool\n        else:\n            # No access restrictions, register as-is\n            self._tools[tool.name] = tool\n\n    async def get_tool(self, name: str) -> Optional[Tool[Any]]:\n        \"\"\"Get a tool by name.\"\"\"\n        return self._tools.get(name)\n\n    async def list_tools(self) -> List[str]:\n        \"\"\"List all registered tool names.\"\"\"\n        return list(self._tools.keys())\n\n    async def get_schemas(self, user: Optional[User] = None) -> List[ToolSchema]:\n        \"\"\"Get schemas for all tools accessible to user.\"\"\"\n        schemas = []\n        for tool in self._tools.values():\n            if user is None or await self._validate_tool_permissions(tool, user):\n                schemas.append(tool.get_schema())\n        return schemas\n\n    async def _validate_tool_permissions(self, tool: Tool[Any], user: User) -> bool:\n        \"\"\"Validate if user has access to tool based on group membership.\n\n        Checks for intersection between user's group memberships and tool's access groups.\n        If tool has no access groups specified, it's accessible to all users.\n        \"\"\"\n        tool_access_groups = tool.access_groups\n        if not tool_access_groups:\n            return True\n\n        user_groups = set(user.group_memberships)\n        tool_groups = set(tool_access_groups)\n        # Grant access if any group in user.group_memberships exists in tool.access_groups\n        return bool(user_groups & tool_groups)\n\n    async def transform_args(\n        self,\n        tool: Tool[T],\n        args: T,\n        user: User,\n        context: ToolContext,\n    ) -> Union[T, ToolRejection]:\n        \"\"\"Transform and validate tool arguments based on user context.\n\n        This method allows per-user transformation of tool arguments, such as:\n        - Applying row-level security (RLS) to SQL queries\n        - Filtering available options based on user permissions\n        - Validating required arguments are present\n        - Redacting sensitive fields\n\n        The default implementation performs no transformation (NoOp).\n        Subclasses can override this method to implement custom transformation logic.\n\n        Args:\n            tool: The tool being executed\n            args: Already Pydantic-validated arguments\n            user: The user executing the tool\n            context: Full execution context\n\n        Returns:\n            Either:\n            - Transformed arguments (may be unchanged if no transformation needed)\n            - ToolRejection with explanation of why args were rejected\n        \"\"\"\n        return args  # Default: no transformation (NoOp)\n\n    async def execute(\n        self,\n        tool_call: ToolCall,\n        context: ToolContext,\n    ) -> ToolResult:\n        \"\"\"Execute a tool call with validation.\"\"\"\n        tool = await self.get_tool(tool_call.name)\n        if not tool:\n            msg = f\"Tool '{tool_call.name}' not found\"\n            return ToolResult(\n                success=False,\n                result_for_llm=msg,\n                ui_component=None,\n                error=msg,\n            )\n\n        # Validate group access\n        if not await self._validate_tool_permissions(tool, context.user):\n            msg = f\"Insufficient group access for tool '{tool_call.name}'\"\n\n            # Audit access denial\n            if (\n                self.audit_logger\n                and self.audit_config\n                and self.audit_config.log_tool_access_checks\n            ):\n                await self.audit_logger.log_tool_access_check(\n                    user=context.user,\n                    tool_name=tool_call.name,\n                    access_granted=False,\n                    required_groups=tool.access_groups,\n                    context=context,\n                    reason=msg,\n                )\n\n            return ToolResult(\n                success=False,\n                result_for_llm=msg,\n                ui_component=None,\n                error=msg,\n            )\n\n        # Validate and parse arguments\n        try:\n            args_model = tool.get_args_schema()\n            validated_args = args_model.model_validate(tool_call.arguments)\n        except Exception as e:\n            msg = f\"Invalid arguments: {str(e)}\"\n            return ToolResult(\n                success=False,\n                result_for_llm=msg,\n                ui_component=None,\n                error=msg,\n            )\n\n        # Transform/validate arguments based on user context\n        transform_result = await self.transform_args(\n            tool=tool,\n            args=validated_args,\n            user=context.user,\n            context=context,\n        )\n\n        if isinstance(transform_result, ToolRejection):\n            return ToolResult(\n                success=False,\n                result_for_llm=transform_result.reason,\n                ui_component=None,\n                error=transform_result.reason,\n            )\n\n        # Use transformed arguments for execution\n        final_args = transform_result\n\n        # Audit successful access check\n        if (\n            self.audit_logger\n            and self.audit_config\n            and self.audit_config.log_tool_access_checks\n        ):\n            await self.audit_logger.log_tool_access_check(\n                user=context.user,\n                tool_name=tool_call.name,\n                access_granted=True,\n                required_groups=tool.access_groups,\n                context=context,\n            )\n\n        # Audit tool invocation\n        if (\n            self.audit_logger\n            and self.audit_config\n            and self.audit_config.log_tool_invocations\n        ):\n            # Get UI features if available from context\n            ui_features = context.metadata.get(\"ui_features_available\", [])\n            await self.audit_logger.log_tool_invocation(\n                user=context.user,\n                tool_call=tool_call,\n                ui_features=ui_features,\n                context=context,\n                sanitize_parameters=self.audit_config.sanitize_tool_parameters,\n            )\n\n        # Execute tool with context-first signature\n        try:\n            start_time = time.perf_counter()\n            result = await tool.execute(context, final_args)\n            execution_time_ms = (time.perf_counter() - start_time) * 1000\n\n            # Add execution time to metadata\n            result.metadata[\"execution_time_ms\"] = execution_time_ms\n\n            # Audit tool result\n            if (\n                self.audit_logger\n                and self.audit_config\n                and self.audit_config.log_tool_results\n            ):\n                await self.audit_logger.log_tool_result(\n                    user=context.user,\n                    tool_call=tool_call,\n                    result=result,\n                    context=context,\n                )\n\n            return result\n        except Exception as e:\n            msg = f\"Execution failed: {str(e)}\"\n            return ToolResult(\n                success=False,\n                result_for_llm=msg,\n                ui_component=None,\n                error=msg,\n            )\n"
  },
  {
    "path": "src/vanna/core/rich_component.py",
    "content": "\"\"\"\nBase classes for rich UI components.\n\nThis module provides the base RichComponent class and supporting enums\nfor the component system.\n\"\"\"\n\nimport uuid\nfrom datetime import datetime\nfrom enum import Enum\nfrom typing import Any, Dict, List, TypeVar\n\nfrom pydantic import BaseModel, Field\n\n# Type variable for self-returning methods\nT = TypeVar(\"T\", bound=\"RichComponent\")\n\n\nclass ComponentType(str, Enum):\n    \"\"\"Types of rich UI components.\"\"\"\n\n    # Basic components\n    TEXT = \"text\"\n    CARD = \"card\"\n    CONTAINER = \"container\"\n\n    # Primitive UI components (domain-agnostic)\n    STATUS_CARD = \"status_card\"\n    PROGRESS_DISPLAY = \"progress_display\"\n    LOG_VIEWER = \"log_viewer\"\n    BADGE = \"badge\"\n    ICON_TEXT = \"icon_text\"\n\n    # Interactive components\n    TASK_LIST = \"task_list\"\n    PROGRESS_BAR = \"progress_bar\"\n    BUTTON = \"button\"\n    BUTTON_GROUP = \"button_group\"\n\n    # Data components\n    TABLE = \"table\"\n    DATAFRAME = \"dataframe\"\n    CHART = \"chart\"\n    CODE_BLOCK = \"code_block\"\n\n    # Status components\n    STATUS_INDICATOR = \"status_indicator\"\n    NOTIFICATION = \"notification\"\n    ALERT = \"alert\"\n\n    # Artifact components\n    ARTIFACT = \"artifact\"\n\n    # UI state components\n    STATUS_BAR_UPDATE = \"status_bar_update\"\n    TASK_TRACKER_UPDATE = \"task_tracker_update\"\n    CHAT_INPUT_UPDATE = \"chat_input_update\"\n\n    # Legacy (deprecated - use primitives instead)\n    TOOL_EXECUTION = \"tool_execution\"\n\n\nclass ComponentLifecycle(str, Enum):\n    \"\"\"Component lifecycle operations.\"\"\"\n\n    CREATE = \"create\"\n    UPDATE = \"update\"\n    REPLACE = \"replace\"\n    REMOVE = \"remove\"\n\n\nclass RichComponent(BaseModel):\n    \"\"\"Base class for all rich UI components.\"\"\"\n\n    id: str = Field(default_factory=lambda: str(uuid.uuid4()))\n    type: ComponentType\n    lifecycle: ComponentLifecycle = ComponentLifecycle.CREATE\n    data: Dict[str, Any] = Field(default_factory=dict)\n    children: List[str] = Field(default_factory=list)  # Child component IDs\n    timestamp: str = Field(default_factory=lambda: datetime.utcnow().isoformat())\n    visible: bool = True\n    interactive: bool = False\n\n    def update(self: T, **kwargs: Any) -> T:\n        \"\"\"Create an updated copy of this component.\"\"\"\n        updated_data = self.model_dump()\n        updated_data.update(kwargs)\n        updated_data[\"lifecycle\"] = ComponentLifecycle.UPDATE\n        updated_data[\"timestamp\"] = datetime.utcnow().isoformat()\n        return self.__class__(**updated_data)\n\n    def hide(self: T) -> T:\n        \"\"\"Create a hidden copy of this component.\"\"\"\n        return self.update(visible=False)\n\n    def show(self: T) -> T:\n        \"\"\"Create a visible copy of this component.\"\"\"\n        return self.update(visible=True)\n\n    def serialize_for_frontend(self) -> Dict[str, Any]:\n        \"\"\"Normalize component payload for the frontend renderer.\n\n        The frontend expects component-specific fields to live under the\n        ``data`` key while the shared metadata (``id``, ``type``, layout hints,\n        etc.) remains at the top level. Pydantic's ``model_dump`` keeps\n        component attributes at the top level, so we remap them here before\n        streaming them across the wire.\n        \"\"\"\n\n        # Base fields that should remain at the top level of the payload.\n        shared_fields = {\n            \"id\",\n            \"type\",\n            \"lifecycle\",\n            \"children\",\n            \"timestamp\",\n            \"visible\",\n            \"interactive\",\n        }\n\n        raw = self.model_dump()\n        payload: Dict[str, Any] = {}\n\n        # Preserve any existing data payload so implementations can opt-in to\n        # advanced usage without losing information.\n        raw_data = raw.get(\"data\")\n        if raw_data is not None and isinstance(raw_data, dict):\n            component_data: Dict[str, Any] = raw_data.copy()\n        else:\n            # Handle case where data might be a sequence or other type, or None\n            component_data = {}\n\n        for key, value in raw.items():\n            if key in shared_fields:\n                payload[key] = value\n            elif key == \"data\":\n                # For most components, skip the base data field\n                continue\n            elif (\n                key == \"rows\"\n                and hasattr(self, \"type\")\n                and self.type.value == \"dataframe\"\n            ):\n                # For DataFrame components, the 'rows' field contains the actual row data\n                # which should be included in the component_data as 'data' for the frontend\n                component_data[\"data\"] = value\n            else:\n                component_data[key] = value\n\n        payload[\"data\"] = component_data\n\n        # Ensure enums are serialized as primitive values for the frontend.\n        payload[\"type\"] = self.type.value\n        payload[\"lifecycle\"] = self.lifecycle.value\n\n        return payload\n"
  },
  {
    "path": "src/vanna/core/simple_component.py",
    "content": "\"\"\"Base classes for simple UI components.\"\"\"\n\nfrom typing import Any, Dict, Optional\nfrom pydantic import BaseModel, Field\nfrom enum import Enum\n\n\nclass SimpleComponentType(str, Enum):\n    TEXT = \"text\"\n    IMAGE = \"image\"\n    LINK = \"link\"\n\n\nclass SimpleComponent(BaseModel):\n    \"\"\"A simple UI component with basic attributes.\"\"\"\n\n    type: SimpleComponentType = Field(..., description=\"Type of the component.\")\n    semantic_type: Optional[str] = Field(\n        default=None, description=\"Semantic type for better categorization.\"\n    )\n    metadata: Optional[Dict[str, Any]] = Field(\n        default=None, description=\"Additional metadata for the component.\"\n    )\n\n    def serialize_for_frontend(self) -> Dict[str, Any]:\n        \"\"\"Serialize simple component for API consumption.\"\"\"\n        return self.model_dump()\n"
  },
  {
    "path": "src/vanna/core/storage/__init__.py",
    "content": "\"\"\"\nStorage domain.\n\nThis module provides the core abstractions for conversation storage in the Vanna Agents framework.\n\"\"\"\n\nfrom .base import ConversationStore\nfrom .models import Conversation, Message\n\n__all__ = [\n    \"ConversationStore\",\n    \"Conversation\",\n    \"Message\",\n]\n"
  },
  {
    "path": "src/vanna/core/storage/base.py",
    "content": "\"\"\"\nStorage domain interface.\n\nThis module contains the abstract base class for conversation storage.\n\"\"\"\n\nfrom abc import ABC, abstractmethod\nfrom typing import List, Optional\n\nfrom .models import Conversation\nfrom ..user.models import User\n\n\nclass ConversationStore(ABC):\n    \"\"\"Abstract base class for conversation storage.\"\"\"\n\n    @abstractmethod\n    async def create_conversation(\n        self, conversation_id: str, user: User, initial_message: str\n    ) -> Conversation:\n        \"\"\"Create a new conversation with the specified ID.\"\"\"\n        pass\n\n    @abstractmethod\n    async def get_conversation(\n        self, conversation_id: str, user: User\n    ) -> Optional[Conversation]:\n        \"\"\"Get conversation by ID, scoped to user.\"\"\"\n        pass\n\n    @abstractmethod\n    async def update_conversation(self, conversation: Conversation) -> None:\n        \"\"\"Update conversation with new messages.\"\"\"\n        pass\n\n    @abstractmethod\n    async def delete_conversation(self, conversation_id: str, user: User) -> bool:\n        \"\"\"Delete conversation.\"\"\"\n        pass\n\n    @abstractmethod\n    async def list_conversations(\n        self, user: User, limit: int = 50, offset: int = 0\n    ) -> List[Conversation]:\n        \"\"\"List conversations for user.\"\"\"\n        pass\n"
  },
  {
    "path": "src/vanna/core/storage/models.py",
    "content": "\"\"\"\nStorage domain models.\n\nThis module contains data models for conversation storage.\n\"\"\"\n\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field\n\nfrom ..tool.models import ToolCall\nfrom ..user.models import User\n\n\nclass Message(BaseModel):\n    \"\"\"Single message in a conversation.\"\"\"\n\n    role: str = Field(description=\"Message role (user/assistant/system/tool)\")\n    content: str = Field(description=\"Message content\")\n    timestamp: datetime = Field(default_factory=datetime.utcnow)\n    metadata: Dict[str, Any] = Field(default_factory=dict)\n    tool_calls: Optional[List[ToolCall]] = Field(default=None)\n    tool_call_id: Optional[str] = Field(\n        default=None, description=\"ID if this is a tool response\"\n    )\n\n\nclass Conversation(BaseModel):\n    \"\"\"Conversation containing multiple messages.\"\"\"\n\n    id: str = Field(description=\"Unique conversation identifier\")\n    user: User = Field(description=\"User this conversation belongs to\")\n    messages: List[Message] = Field(\n        default_factory=list, description=\"Messages in conversation\"\n    )\n    created_at: datetime = Field(default_factory=datetime.utcnow)\n    updated_at: datetime = Field(default_factory=datetime.utcnow)\n    metadata: Dict[str, Any] = Field(\n        default_factory=dict, description=\"Additional conversation metadata\"\n    )\n\n    def add_message(self, message: Message) -> None:\n        \"\"\"Add a message to the conversation.\"\"\"\n        self.messages.append(message)\n        self.updated_at = datetime.utcnow()\n"
  },
  {
    "path": "src/vanna/core/system_prompt/__init__.py",
    "content": "\"\"\"\nSystem prompt domain.\n\nThis module provides the core abstractions for building system prompts in the Vanna Agents framework.\n\"\"\"\n\nfrom .base import SystemPromptBuilder\nfrom .default import DefaultSystemPromptBuilder\n\n__all__ = [\n    \"SystemPromptBuilder\",\n    \"DefaultSystemPromptBuilder\",\n]\n"
  },
  {
    "path": "src/vanna/core/system_prompt/base.py",
    "content": "\"\"\"\nSystem prompt builder interface.\n\nThis module contains the abstract base class for system prompt builders.\n\"\"\"\n\nfrom abc import ABC, abstractmethod\nfrom typing import TYPE_CHECKING, List, Optional\n\nif TYPE_CHECKING:\n    from ..tool.models import ToolSchema\n    from ..user.models import User\n\n\nclass SystemPromptBuilder(ABC):\n    \"\"\"Abstract base class for system prompt builders.\n\n    Subclasses should implement the build_system_prompt method to generate\n    system prompts based on user context and available tools.\n    \"\"\"\n\n    @abstractmethod\n    async def build_system_prompt(\n        self, user: \"User\", tools: List[\"ToolSchema\"]\n    ) -> Optional[str]:\n        \"\"\"\n        Build a system prompt based on user context and available tools.\n\n        Args:\n            user: The user making the request\n            tools: List of tools available to the user\n\n        Returns:\n            System prompt string, or None if no system prompt should be used\n        \"\"\"\n        pass\n"
  },
  {
    "path": "src/vanna/core/system_prompt/default.py",
    "content": "\"\"\"\nDefault system prompt builder implementation with memory workflow support.\n\nThis module provides a default implementation of the SystemPromptBuilder interface\nthat automatically includes memory workflow instructions when memory tools are available.\n\"\"\"\n\nfrom typing import TYPE_CHECKING, List, Optional\nfrom datetime import datetime\n\nfrom .base import SystemPromptBuilder\n\nif TYPE_CHECKING:\n    from ..tool.models import ToolSchema\n    from ..user.models import User\n\n\nclass DefaultSystemPromptBuilder(SystemPromptBuilder):\n    \"\"\"Default system prompt builder with automatic memory workflow integration.\n\n    Dynamically generates system prompts that include memory workflow\n    instructions when memory tools (search_saved_correct_tool_uses and\n    save_question_tool_args) are available.\n    \"\"\"\n\n    def __init__(self, base_prompt: Optional[str] = None):\n        \"\"\"Initialize with an optional base prompt.\n\n        Args:\n            base_prompt: Optional base system prompt. If not provided, uses a default.\n        \"\"\"\n        self.base_prompt = base_prompt\n\n    async def build_system_prompt(\n        self, user: \"User\", tools: List[\"ToolSchema\"]\n    ) -> Optional[str]:\n        \"\"\"\n        Build a system prompt with memory workflow instructions.\n\n        Args:\n            user: The user making the request\n            tools: List of tools available to the user\n\n        Returns:\n            System prompt string with memory workflow instructions if applicable\n        \"\"\"\n        if self.base_prompt is not None:\n            return self.base_prompt\n\n        # Check which memory tools are available\n        tool_names = [tool.name for tool in tools]\n        has_search = \"search_saved_correct_tool_uses\" in tool_names\n        has_save = \"save_question_tool_args\" in tool_names\n        has_text_memory = \"save_text_memory\" in tool_names\n\n        # Get today's date\n        today_date = datetime.now().strftime(\"%Y-%m-%d\")\n\n        # Base system prompt\n        prompt_parts = [\n            f\"You are Vanna, an AI data analyst assistant created to help users with data analysis tasks. Today's date is {today_date}.\",\n            \"\",\n            \"Response Guidelines:\",\n            \"- Any summary of what you did or observations should be the final step.\",\n            \"- Use the available tools to help the user accomplish their goals.\",\n            \"- When you execute a query, that raw result is shown to the user outside of your response so YOU DO NOT need to include it in your response. Focus on summarizing and interpreting the results.\",\n        ]\n\n        if tools:\n            prompt_parts.append(\n                f\"\\nYou have access to the following tools: {', '.join(tool_names)}\"\n            )\n\n        # Add memory workflow instructions based on available tools\n        if has_search or has_save or has_text_memory:\n            prompt_parts.append(\"\\n\" + \"=\" * 60)\n            prompt_parts.append(\"MEMORY SYSTEM:\")\n            prompt_parts.append(\"=\" * 60)\n\n        if has_search or has_save:\n            prompt_parts.append(\"\\n1. TOOL USAGE MEMORY (Structured Workflow):\")\n            prompt_parts.append(\"-\" * 50)\n\n        if has_search:\n            prompt_parts.extend(\n                [\n                    \"\",\n                    \"• BEFORE executing any tool (run_sql, visualize_data, or calculator), you MUST first call search_saved_correct_tool_uses with the user's question to check if there are existing successful patterns for similar questions.\",\n                    \"\",\n                    \"• Review the search results (if any) to inform your approach before proceeding with other tool calls.\",\n                ]\n            )\n\n        if has_save:\n            prompt_parts.extend(\n                [\n                    \"\",\n                    \"• AFTER successfully executing a tool that produces correct and useful results, you MUST call save_question_tool_args to save the successful pattern for future use.\",\n                ]\n            )\n\n        if has_search or has_save:\n            prompt_parts.extend(\n                [\n                    \"\",\n                    \"Example workflow:\",\n                    \"  • User asks a question\",\n                    f'  • First: Call search_saved_correct_tool_uses(question=\"user\\'s question\")'\n                    if has_search\n                    else \"\",\n                    \"  • Then: Execute the appropriate tool(s) based on search results and the question\",\n                    f'  • Finally: If successful, call save_question_tool_args(question=\"user\\'s question\", tool_name=\"tool_used\", args={{the args you used}})'\n                    if has_save\n                    else \"\",\n                    \"\",\n                    \"Do NOT skip the search step, even if you think you know how to answer. Do NOT forget to save successful executions.\"\n                    if has_search\n                    else \"\",\n                    \"\",\n                    \"The only exceptions to searching first are:\",\n                    '  • When the user is explicitly asking about the tools themselves (like \"list the tools\")',\n                    \"  • When the user is testing or asking you to demonstrate the save/search functionality itself\",\n                ]\n            )\n\n        if has_text_memory:\n            prompt_parts.extend(\n                [\n                    \"\",\n                    \"2. TEXT MEMORY (Domain Knowledge & Context):\",\n                    \"-\" * 50,\n                    \"\",\n                    \"• save_text_memory: Save important context about the database, schema, or domain\",\n                    \"\",\n                    \"Use text memory to save:\",\n                    \"  • Database schema details (column meanings, data types, relationships)\",\n                    \"  • Company-specific terminology and definitions\",\n                    \"  • Query patterns or best practices for this database\",\n                    \"  • Domain knowledge about the business or data\",\n                    \"  • User preferences for queries or visualizations\",\n                    \"\",\n                    \"DO NOT save:\",\n                    \"  • Information already captured in tool usage memory\",\n                    \"  • One-time query results or temporary observations\",\n                    \"\",\n                    \"Examples:\",\n                    '  • save_text_memory(content=\"The status column uses 1 for active, 0 for inactive\")',\n                    '  • save_text_memory(content=\"MRR means Monthly Recurring Revenue in our schema\")',\n                    \"  • save_text_memory(content=\\\"Always exclude test accounts where email contains 'test'\\\")\",\n                ]\n            )\n\n        if has_search or has_save or has_text_memory:\n            # Remove empty strings from the list\n            prompt_parts = [part for part in prompt_parts if part != \"\"]\n\n        return \"\\n\".join(prompt_parts)\n"
  },
  {
    "path": "src/vanna/core/tool/__init__.py",
    "content": "\"\"\"\nTool domain.\n\nThis module provides the core abstractions for tools in the Vanna Agents framework.\n\"\"\"\n\nfrom .base import T, Tool\nfrom .models import ToolCall, ToolContext, ToolRejection, ToolResult, ToolSchema\n\n__all__ = [\n    \"Tool\",\n    \"T\",\n    \"ToolCall\",\n    \"ToolContext\",\n    \"ToolRejection\",\n    \"ToolResult\",\n    \"ToolSchema\",\n]\n"
  },
  {
    "path": "src/vanna/core/tool/base.py",
    "content": "\"\"\"\nTool domain interface.\n\nThis module contains the abstract base class for tools.\n\"\"\"\n\nfrom abc import ABC, abstractmethod\nfrom typing import Generic, List, Type, TypeVar\n\nfrom .models import ToolContext, ToolResult, ToolSchema\n\n# Type variable for tool argument types\nT = TypeVar(\"T\")\n\n\nclass Tool(ABC, Generic[T]):\n    \"\"\"Abstract base class for tools.\"\"\"\n\n    @property\n    @abstractmethod\n    def name(self) -> str:\n        \"\"\"Unique name for this tool.\"\"\"\n        pass\n\n    @property\n    @abstractmethod\n    def description(self) -> str:\n        \"\"\"Description of what this tool does.\"\"\"\n        pass\n\n    @property\n    def access_groups(self) -> List[str]:\n        \"\"\"Groups permitted to access this tool.\"\"\"\n        return []\n\n    @abstractmethod\n    def get_args_schema(self) -> Type[T]:\n        \"\"\"Return the Pydantic model for arguments.\"\"\"\n        pass\n\n    @abstractmethod\n    async def execute(self, context: ToolContext, args: T) -> ToolResult:\n        \"\"\"Execute the tool with validated arguments.\n\n        Args:\n            context: Execution context containing user, conversation_id, and request_id\n            args: Validated tool arguments\n\n        Returns:\n            ToolResult with success status, result for LLM, and optional UI component\n        \"\"\"\n        pass\n\n    def get_schema(self) -> ToolSchema:\n        \"\"\"Generate tool schema for LLM.\"\"\"\n        from typing import Any, cast\n\n        args_model = self.get_args_schema()\n        # Get the schema - args_model should be a Pydantic model class\n        schema = (\n            cast(Any, args_model).model_json_schema()\n            if hasattr(args_model, \"model_json_schema\")\n            else {}\n        )\n        return ToolSchema(\n            name=self.name,\n            description=self.description,\n            parameters=schema,\n            access_groups=self.access_groups,\n        )\n"
  },
  {
    "path": "src/vanna/core/tool/models.py",
    "content": "\"\"\"\nTool domain models.\n\nThis module contains data models for tool execution.\n\"\"\"\n\nfrom typing import TYPE_CHECKING, Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, Field\n\n# Import AgentMemory at runtime for Pydantic model resolution\nfrom vanna.capabilities.agent_memory import AgentMemory\n\nif TYPE_CHECKING:\n    from ..components import UiComponent\n    from ..user.models import User\n    from ..observability import ObservabilityProvider\n\n\nclass ToolCall(BaseModel):\n    \"\"\"Represents a tool call from the LLM.\"\"\"\n\n    id: str = Field(description=\"Unique identifier for this tool call\")\n    name: str = Field(description=\"Name of the tool to execute\")\n    arguments: Dict[str, Any] = Field(description=\"Raw arguments from LLM\")\n\n\nclass ToolContext(BaseModel):\n    \"\"\"Context passed to all tool executions.\"\"\"\n\n    user: \"User\"  # Forward reference to avoid circular import\n    conversation_id: str\n    request_id: str = Field(description=\"Unique request identifier for tracing\")\n    agent_memory: AgentMemory = Field(\n        description=\"Agent memory for tool usage learning\"\n    )\n    metadata: Dict[str, Any] = Field(default_factory=dict)\n    observability_provider: Optional[\"ObservabilityProvider\"] = Field(\n        default=None,\n        description=\"Optional observability provider for metrics and spans\",\n    )\n\n    class Config:\n        arbitrary_types_allowed = True\n\n\nclass ToolResult(BaseModel):\n    \"\"\"Result from tool execution.\n\n    Changes:\n    - `result_for_llm`: string that will be sent back to the LLM.\n    - `ui_component`: optional UI payload for rendering in clients.\n    \"\"\"\n\n    success: bool = Field(description=\"Whether execution succeeded\")\n    result_for_llm: str = Field(description=\"String content to send back to the LLM\")\n    ui_component: Optional[\"UiComponent\"] = Field(\n        default=None, description=\"Optional UI component for rendering\"\n    )\n    error: Optional[str] = Field(default=None, description=\"Error message if failed\")\n    metadata: Dict[str, Any] = Field(default_factory=dict)\n\n\nclass ToolSchema(BaseModel):\n    \"\"\"Schema describing a tool for LLM consumption.\"\"\"\n\n    name: str = Field(description=\"Tool name\")\n    description: str = Field(description=\"What this tool does\")\n    parameters: Dict[str, Any] = Field(description=\"JSON Schema of parameters\")\n    access_groups: List[str] = Field(\n        default_factory=list, description=\"Groups permitted to access this tool\"\n    )\n\n\nclass ToolRejection(BaseModel):\n    \"\"\"Indicates tool execution should be rejected with a message.\n\n    Used by transform_args to reject tool execution when arguments\n    cannot be appropriately transformed for the user's context.\n    \"\"\"\n\n    reason: str = Field(\n        description=\"Explanation of why the tool execution was rejected\"\n    )\n"
  },
  {
    "path": "src/vanna/core/user/__init__.py",
    "content": "\"\"\"\nUser domain.\n\nThis module provides the core abstractions for user management in the Vanna Agents framework.\n\"\"\"\n\nfrom .base import UserService\nfrom .models import User\nfrom .resolver import UserResolver\nfrom .request_context import RequestContext\n\n__all__ = [\n    \"UserService\",\n    \"User\",\n    \"UserResolver\",\n    \"RequestContext\",\n]\n"
  },
  {
    "path": "src/vanna/core/user/base.py",
    "content": "\"\"\"\nUser domain interface.\n\nThis module contains the abstract base class for user services.\n\"\"\"\n\nfrom abc import ABC, abstractmethod\nfrom typing import Any, Dict, Optional\n\nfrom .models import User\n\n\nclass UserService(ABC):\n    \"\"\"Service for user management and authentication.\"\"\"\n\n    @abstractmethod\n    async def get_user(self, user_id: str) -> Optional[User]:\n        \"\"\"Get user by ID.\"\"\"\n        pass\n\n    @abstractmethod\n    async def authenticate(self, credentials: Dict[str, Any]) -> Optional[User]:\n        \"\"\"Authenticate user and return User object if successful.\"\"\"\n        pass\n\n    @abstractmethod\n    async def has_permission(self, user: User, permission: str) -> bool:\n        \"\"\"Check if user has specific permission.\"\"\"\n        pass\n"
  },
  {
    "path": "src/vanna/core/user/models.py",
    "content": "\"\"\"\nUser domain models.\n\nThis module contains data models for user management.\n\"\"\"\n\nfrom typing import Any, Dict, List, Optional\n\nfrom pydantic import BaseModel, ConfigDict, Field\n\n\nclass User(BaseModel):\n    \"\"\"User model for authentication and scoping.\"\"\"\n\n    id: str = Field(description=\"Unique user identifier\")\n    username: Optional[str] = Field(default=None, description=\"Username\")\n    email: Optional[str] = Field(default=None, description=\"User email\")\n    metadata: Dict[str, Any] = Field(\n        default_factory=dict, description=\"Additional user metadata\"\n    )\n    group_memberships: List[str] = Field(\n        default_factory=list, description=\"Groups the user belongs to\"\n    )\n\n    model_config = ConfigDict(extra=\"allow\")\n"
  },
  {
    "path": "src/vanna/core/user/request_context.py",
    "content": "\"\"\"\nRequest context for user resolution.\n\nThis module provides the RequestContext model for passing web request\ninformation to UserResolver implementations.\n\"\"\"\n\nfrom typing import Any, Dict, Optional\n\nfrom pydantic import BaseModel, Field\n\n\nclass RequestContext(BaseModel):\n    \"\"\"Context from a web request for user resolution.\n\n    This structured object replaces raw dictionaries for passing request\n    data to UserResolver implementations, making it easier to access\n    cookies, headers, and other request metadata.\n\n    Example:\n        context = RequestContext(\n            cookies={'vanna_email': 'alice@example.com'},\n            headers={'Authorization': 'Bearer token'},\n            remote_addr='127.0.0.1'\n        )\n        user = await resolver.resolve_user(context)\n    \"\"\"\n\n    cookies: Dict[str, str] = Field(default_factory=dict, description=\"Request cookies\")\n\n    headers: Dict[str, str] = Field(default_factory=dict, description=\"Request headers\")\n\n    remote_addr: Optional[str] = Field(default=None, description=\"Remote IP address\")\n\n    query_params: Dict[str, str] = Field(\n        default_factory=dict, description=\"Query parameters\"\n    )\n\n    metadata: Dict[str, Any] = Field(\n        default_factory=dict, description=\"Additional framework-specific metadata\"\n    )\n\n    def get_cookie(self, name: str, default: Optional[str] = None) -> Optional[str]:\n        \"\"\"Get cookie value by name.\n\n        Args:\n            name: Cookie name\n            default: Default value if cookie not found\n\n        Returns:\n            Cookie value or default\n        \"\"\"\n        return self.cookies.get(name, default)\n\n    def get_header(self, name: str, default: Optional[str] = None) -> Optional[str]:\n        \"\"\"Get header value by name (case-insensitive).\n\n        Args:\n            name: Header name\n            default: Default value if header not found\n\n        Returns:\n            Header value or default\n        \"\"\"\n        # Case-insensitive header lookup\n        name_lower = name.lower()\n        for key, value in self.headers.items():\n            if key.lower() == name_lower:\n                return value\n        return default\n"
  },
  {
    "path": "src/vanna/core/user/resolver.py",
    "content": "\"\"\"\nUser resolver interface for web request authentication.\n\nThis module provides the abstract base class for resolving web requests\nto authenticated User objects.\n\"\"\"\n\nfrom abc import ABC, abstractmethod\n\nfrom .models import User\nfrom .request_context import RequestContext\n\n\nclass UserResolver(ABC):\n    \"\"\"Resolves web requests to authenticated users.\n\n    Implementations of this interface handle the specifics of extracting\n    user identity from request context (cookies, headers, tokens, etc.)\n    and creating authenticated User objects.\n\n    Example:\n        class JwtUserResolver(UserResolver):\n            async def resolve_user(self, request_context: RequestContext) -> User:\n                token = request_context.get_header('Authorization')\n                # ... validate JWT and extract user info\n                return User(id=user_id, username=username, email=email)\n    \"\"\"\n\n    @abstractmethod\n    async def resolve_user(self, request_context: RequestContext) -> User:\n        \"\"\"Resolve user from request context.\n\n        Args:\n            request_context: Structured request context with cookies, headers, etc.\n\n        Returns:\n            Authenticated User object\n\n        Raises:\n            Can raise exceptions for authentication failures\n        \"\"\"\n        pass\n"
  },
  {
    "path": "src/vanna/core/validation.py",
    "content": "\"\"\"\nDevelopment utilities for validating Pydantic models.\n\nThis module provides utilities that can be used during development\nand testing to catch forward reference issues early.\n\"\"\"\n\nfrom typing import Any, Dict, List, Tuple, Type\nfrom pydantic import BaseModel\nimport importlib\nimport inspect\n\n\ndef validate_pydantic_models_in_package(package_name: str) -> Dict[str, Any]:\n    \"\"\"\n    Validate all Pydantic models in a package for completeness.\n\n    This function can be used in tests or development scripts to catch\n    forward reference issues before they cause runtime errors.\n\n    Args:\n        package_name: Name of the package to validate (e.g., 'vanna.core')\n\n    Returns:\n        Dictionary with validation results\n    \"\"\"\n    results: Dict[str, Any] = {\n        \"total_models\": 0,\n        \"incomplete_models\": [],\n        \"models\": {},\n        \"summary\": \"\",\n    }\n\n    try:\n        # Import the package\n        package = importlib.import_module(package_name)\n\n        # Get all submodules\n        submodules = []\n        if hasattr(package, \"__path__\"):\n            import pkgutil\n\n            for _, name, _ in pkgutil.iter_modules(\n                package.__path__, package_name + \".\"\n            ):\n                try:\n                    submodule = importlib.import_module(name)\n                    submodules.append((name, submodule))\n                except ImportError:\n                    continue\n        else:\n            submodules = [(package_name, package)]\n\n        # Check all Pydantic models in each submodule\n        for module_name, module in submodules:\n            for name, obj in inspect.getmembers(module):\n                if (\n                    inspect.isclass(obj)\n                    and issubclass(obj, BaseModel)\n                    and obj is not BaseModel\n                ):\n                    model_key = f\"{module_name}.{name}\"\n                    results[\"total_models\"] += 1\n\n                    # Check for forward references\n                    forward_refs: List[Tuple[str, str]] = []\n                    for field_name, field_info in obj.model_fields.items():\n                        annotation = field_info.annotation\n                        if annotation is not None and hasattr(\n                            annotation, \"__forward_arg__\"\n                        ):\n                            forward_refs.append(\n                                (field_name, annotation.__forward_arg__)\n                            )\n\n                    # Check completeness\n                    try:\n                        obj.model_json_schema()\n                        is_complete = True\n                        error = None\n                    except Exception as e:\n                        is_complete = False\n                        error = str(e)\n                        results[\"incomplete_models\"].append(model_key)\n\n                    results[\"models\"][model_key] = {\n                        \"class\": obj,\n                        \"forward_references\": forward_refs,\n                        \"is_complete\": is_complete,\n                        \"error\": error,\n                    }\n\n        # Generate summary\n        incomplete_models = results[\"incomplete_models\"]\n        incomplete_count = len(incomplete_models)\n        total_models = results[\"total_models\"]\n        if incomplete_count == 0:\n            results[\"summary\"] = (\n                f\"✓ All {total_models} Pydantic models are complete and valid!\"\n            )\n        else:\n            results[\"summary\"] = (\n                f\"⚠ {incomplete_count} of {total_models} models are incomplete: \"\n                f\"{', '.join(incomplete_models)}\"\n            )\n\n    except Exception as e:\n        results[\"summary\"] = f\"Error validating package {package_name}: {e}\"\n\n    return results\n\n\ndef check_models_health() -> bool:\n    \"\"\"\n    Quick health check for all core Pydantic models.\n\n    Returns:\n        True if all models are healthy, False otherwise\n    \"\"\"\n    core_packages = [\n        \"vanna.core.tool.models\",\n        \"vanna.core.user.models\",\n        \"vanna.core.llm.models\",\n        \"vanna.core.storage.models\",\n        \"vanna.core.agent.models\",\n    ]\n\n    all_healthy = True\n\n    for package in core_packages:\n        try:\n            results = validate_pydantic_models_in_package(package)\n            if results[\"incomplete_models\"]:\n                print(f\"❌ Issues in {package}: {results['incomplete_models']}\")\n                all_healthy = False\n            else:\n                print(f\"✅ {package}: {results['total_models']} models OK\")\n        except Exception as e:\n            print(f\"❌ Error checking {package}: {e}\")\n            all_healthy = False\n\n    return all_healthy\n\n\nif __name__ == \"__main__\":\n    print(\"Checking Pydantic model health across core packages...\")\n    print(\"=\" * 60)\n\n    healthy = check_models_health()\n\n    print(\"=\" * 60)\n    if healthy:\n        print(\"🎉 All Pydantic models are healthy!\")\n    else:\n        print(\"⚠️  Some models need attention.\")\n        print(\"\\nTo fix forward reference issues:\")\n        print(\"1. Ensure all referenced classes are imported\")\n        print(\"2. Call model_rebuild() after imports\")\n        print(\"3. Use proper TYPE_CHECKING imports for circular deps\")\n\n    print(\"\\nNote: You can also catch these issues at development time using:\")\n    print(\"  - mypy static type checking\")\n    print(\"  - This validation script in your test suite\")\n    print(\"  - Pre-commit hooks\")\n"
  },
  {
    "path": "src/vanna/core/workflow/__init__.py",
    "content": "\"\"\"\nWorkflow handler system for deterministic workflow execution.\n\nThis module provides the WorkflowHandler interface for intercepting user messages\nand executing deterministic workflows before they reach the LLM. This is useful\nfor command handling, pattern-based routing, and state-based workflows.\n\"\"\"\n\nfrom .base import WorkflowHandler, WorkflowResult\nfrom .default import DefaultWorkflowHandler\n\n__all__ = [\"WorkflowHandler\", \"WorkflowResult\", \"DefaultWorkflowHandler\"]\n"
  },
  {
    "path": "src/vanna/core/workflow/base.py",
    "content": "\"\"\"\nBase workflow handler interface.\n\nWorkflow triggers allow you to execute deterministic workflows in response to\nuser messages before they are sent to the LLM. This is useful for:\n- Command handling (e.g., /help, /reset)\n- Pattern-based routing (e.g., report generation)\n- State-based workflows (e.g., onboarding flows)\n- Quota enforcement with custom responses\n\"\"\"\n\nfrom abc import ABC, abstractmethod\nfrom typing import (\n    TYPE_CHECKING,\n    Optional,\n    Union,\n    List,\n    AsyncGenerator,\n    Callable,\n    Awaitable,\n)\nfrom dataclasses import dataclass\n\nif TYPE_CHECKING:\n    from ..user.models import User\n    from ..storage import Conversation\n    from ...components import UiComponent\n    from ..agent.agent import Agent\n\n\n@dataclass\nclass WorkflowResult:\n    \"\"\"Result from a workflow handler attempt.\n\n    When a workflow handles a message, it can optionally return UI components to stream\n    to the user and/or mutate the conversation state.\n\n    Attributes:\n        should_skip_llm: If True, the workflow handled the message and LLM processing is skipped.\n                         If False, the message continues to the agent/LLM.\n        components: Optional UI components to stream back to the user.\n                    Can be a list or async generator for streaming responses.\n        conversation_mutation: Optional async callback to modify conversation state\n                               (e.g., clearing messages, adding system events).\n\n    Example:\n        # Simple command response\n        WorkflowResult(\n            should_skip_llm=True,\n            components=[RichTextComponent(content=\"Help text here\")]\n        )\n\n        # With conversation mutation\n        async def clear_history(conv):\n            conv.messages.clear()\n\n        WorkflowResult(\n            should_skip_llm=True,\n            components=[StatusCardComponent(...)],\n            conversation_mutation=clear_history\n        )\n\n        # Not handled, continue to agent\n        WorkflowResult(should_skip_llm=False)\n    \"\"\"\n\n    should_skip_llm: bool\n    components: Optional[\n        Union[List[\"UiComponent\"], AsyncGenerator[\"UiComponent\", None]]\n    ] = None\n    conversation_mutation: Optional[Callable[[\"Conversation\"], Awaitable[None]]] = None\n\n\nclass WorkflowHandler(ABC):\n    \"\"\"Base class for handling deterministic workflows before LLM processing.\n\n    Implement this interface to intercept user messages and execute deterministic\n    workflows instead of sending to the LLM. This is the first extensibility point\n    in the agent's message processing pipeline, running after user resolution and\n    conversation loading but before the message is added to conversation history\n    or sent to the LLM.\n\n    Use cases:\n    - Slash commands (/help, /reset, /report)\n    - Pattern-based routing (regex matching)\n    - State-based workflows (onboarding, surveys)\n    - Custom quota enforcement with helpful messages\n    - Deterministic report generation\n    - Starter UI (buttons, welcome messages) when conversation begins\n\n    Example:\n        class CommandWorkflow(WorkflowHandler):\n            async def try_handle(self, agent, user, conversation, message):\n                if message.startswith(\"/help\"):\n                    return WorkflowResult(\n                        should_skip_llm=True,\n                        components=[\n                            RichTextComponent(\n                                content=\"Available commands:\\\\n- /help\\\\n- /reset\",\n                                markdown=True\n                            )\n                        ]\n                    )\n\n                # Execute tool for reports\n                if message.startswith(\"/report\"):\n                    tool = await agent.tool_registry.get_tool(\"generate_report\")\n                    result = await tool.execute(ToolContext(user=user), {})\n                    return WorkflowResult(should_skip_llm=True, components=[result.ui_component])\n\n                # Not handled, continue to agent\n                return WorkflowResult(should_skip_llm=False)\n\n            async def get_starter_ui(self, agent, user, conversation):\n                return [\n                    RichTextComponent(content=f\"Welcome {user.username}!\"),\n                    ButtonComponent(label=\"Generate Report\", value=\"/report\"),\n                ]\n\n        agent = Agent(\n            llm_service=...,\n            tool_registry=...,\n            user_resolver=...,\n            workflow_handler=CommandWorkflow()\n        )\n\n    Observability:\n        The agent automatically creates an \"agent.workflow_handler\" span when\n        a WorkflowHandler is configured, allowing you to monitor handler\n        performance and outcomes.\n    \"\"\"\n\n    @abstractmethod\n    async def try_handle(\n        self, agent: \"Agent\", user: \"User\", conversation: \"Conversation\", message: str\n    ) -> WorkflowResult:\n        \"\"\"Attempt to handle a workflow for the given message.\n\n        This method is called for every user message before it reaches the LLM.\n        Inspect the message content, user context, and conversation state to\n        decide whether to execute a deterministic workflow or allow normal\n        agent processing.\n\n        Args:\n            agent: The agent instance, providing access to tool_registry, config,\n                   and observability_provider for tool execution and logging.\n            user: The user who sent the message, including their ID, permissions,\n                  and metadata. Use this for permission checks or personalization.\n            conversation: The current conversation context, including message history.\n                          Can be inspected for state-based workflows.\n            message: The user's raw message content.\n\n        Returns:\n            WorkflowResult with should_skip_llm=True to execute a workflow and skip LLM,\n            or should_skip_llm=False to continue normal agent processing.\n\n            When should_skip_llm=True:\n            - The message is NOT added to conversation history automatically\n            - The components are streamed to the user\n            - The conversation_mutation callback (if provided) is executed\n            - The agent returns without calling the LLM\n\n            When should_skip_llm=False:\n            - The message is added to conversation history\n            - Normal agent processing continues (LLM call, tool execution, etc.)\n\n        Example:\n            async def try_handle(self, agent, user, conversation, message):\n                # Pattern matching with tool execution\n                if message.startswith(\"/report\"):\n                    # Execute tool from registry\n                    tool = await agent.tool_registry.get_tool(\"generate_sales_report\")\n                    context = ToolContext(user=user, conversation=conversation)\n                    result = await tool.execute(context, {})\n\n                    return WorkflowResult(\n                        should_skip_llm=True,\n                        components=[...]\n                    )\n\n                # State-based workflow\n                if user.metadata.get(\"needs_onboarding\"):\n                    return await self._onboarding_flow(agent, user, message)\n\n                # Permission check\n                if message.startswith(\"/admin\") and \"admin\" not in user.permissions:\n                    return WorkflowResult(\n                        should_skip_llm=True,\n                        components=[RichTextComponent(content=\"Access denied.\")]\n                    )\n\n                # Continue to agent\n                return WorkflowResult(should_skip_llm=False)\n        \"\"\"\n        pass\n\n    async def get_starter_ui(\n        self, agent: \"Agent\", user: \"User\", conversation: \"Conversation\"\n    ) -> Optional[List[\"UiComponent\"]]:\n        \"\"\"Provide UI components when a conversation starts.\n\n        Override this method to show starter buttons, welcome messages,\n        or quick actions when a new chat is opened by the user.\n\n        This is called by the frontend/server when initializing a new\n        conversation, before any user messages are sent.\n\n        Args:\n            agent: The agent instance, providing access to tool_registry, config,\n                   and observability_provider for dynamic UI generation.\n            user: The user starting the conversation\n            conversation: The new conversation (typically empty)\n\n        Returns:\n            List of UI components to display, or None for no starter UI.\n            Components can include buttons, welcome text, quick actions, etc.\n\n        Example:\n            async def get_starter_ui(self, agent, user, conversation):\n                # Show role-based quick actions\n                if \"analyst\" in user.permissions:\n                    # Dynamically generate buttons based on available tools\n                    report_tools = [\n                        tool for tool in agent.tool_registry.list_tools()\n                        if tool.startswith(\"report_\")\n                    ]\n\n                    buttons = [\n                        ButtonComponent(label=f\"📊 {tool}\", value=f\"/{tool}\")\n                        for tool in report_tools\n                    ]\n\n                    return [\n                        RichTextComponent(\n                            content=f\"Welcome back, {user.username}!\",\n                            markdown=True\n                        ),\n                        *buttons\n                    ]\n\n                # New user onboarding\n                if user.metadata.get(\"is_new_user\"):\n                    return [\n                        RichTextComponent(\n                            content=\"# Welcome to Vanna!\\\\n\\\\nTry one of these to get started:\",\n                            markdown=True\n                        ),\n                        ButtonComponent(label=\"Show Example Query\", value=\"/example\"),\n                        ButtonComponent(label=\"View Tutorial\", value=\"/tutorial\"),\n                    ]\n\n                return None\n        \"\"\"\n        return None\n"
  },
  {
    "path": "src/vanna/core/workflow/default.py",
    "content": "\"\"\"\nDefault workflow handler implementation with setup health checking.\n\nThis module provides a default implementation of the WorkflowHandler interface\nthat provides a smart starter UI based on available tools and setup status.\n\"\"\"\n\nfrom typing import TYPE_CHECKING, List, Optional, Dict, Any\nimport traceback\nimport uuid\nfrom .base import WorkflowHandler, WorkflowResult\n\nif TYPE_CHECKING:\n    from ..agent.agent import Agent\n    from ..user.models import User\n    from ..storage import Conversation\n\n# Import components at module level to avoid circular imports\nfrom vanna.components import (\n    UiComponent,\n    RichTextComponent,\n    StatusCardComponent,\n    ButtonComponent,\n    ButtonGroupComponent,\n    SimpleTextComponent,\n    CardComponent,\n)\n\n# Note: StatusCardComponent and ButtonGroupComponent are kept for /status command compatibility\n\n\nclass DefaultWorkflowHandler(WorkflowHandler):\n    \"\"\"Default workflow handler that provides setup health checking and starter UI.\n\n    This handler provides a starter UI that:\n    - Checks if run_sql tool is available (critical)\n    - Checks if memory tools are available (warning if missing)\n    - Checks if visualization tools are available\n    - Provides appropriate setup guidance based on what's missing\n    \"\"\"\n\n    def __init__(self, welcome_message: Optional[str] = None):\n        \"\"\"Initialize with optional custom welcome message.\n\n        Args:\n            welcome_message: Optional custom welcome message. If not provided,\n                           generates one based on available tools.\n        \"\"\"\n        self.welcome_message = welcome_message\n\n    async def try_handle(\n        self, agent: \"Agent\", user: \"User\", conversation: \"Conversation\", message: str\n    ) -> WorkflowResult:\n        \"\"\"Handle basic commands, but mostly passes through to LLM.\"\"\"\n\n        # Handle basic help command\n        if message.strip().lower() in [\"/help\", \"help\", \"/h\"]:\n            # Check if user is admin\n            is_admin = \"admin\" in user.group_memberships\n\n            help_content = (\n                \"## 🤖 Vanna AI Assistant\\n\\n\"\n                \"I'm your AI data analyst! Here's what I can help you with:\\n\\n\"\n                \"**💬 Natural Language Queries**\\n\"\n                '- \"Show me sales data for last quarter\"\\n'\n                '- \"Which customers have the highest orders?\"\\n'\n                '- \"Create a chart of revenue by month\"\\n\\n'\n                \"**🔧 Commands**\\n\"\n                \"- `/help` - Show this help message\\n\"\n            )\n\n            if is_admin:\n                help_content += (\n                    \"\\n**🔒 Admin Commands**\\n\"\n                    \"- `/status` - Check setup status\\n\"\n                    \"- `/memories` - View and manage recent memories\\n\"\n                    \"- `/delete [id]` - Delete a memory by ID\\n\"\n                )\n\n            help_content += \"\\n\\nJust ask me anything about your data in plain English!\"\n\n            return WorkflowResult(\n                should_skip_llm=True,\n                components=[\n                    UiComponent(\n                        rich_component=RichTextComponent(\n                            content=help_content,\n                            markdown=True,\n                        ),\n                        simple_component=None,\n                    )\n                ],\n            )\n\n        # Handle status check command (admin-only)\n        if message.strip().lower() in [\"/status\", \"status\"]:\n            # Check if user is admin\n            if \"admin\" not in user.group_memberships:\n                return WorkflowResult(\n                    should_skip_llm=True,\n                    components=[\n                        UiComponent(\n                            rich_component=RichTextComponent(\n                                content=\"# 🔒 Access Denied\\n\\n\"\n                                \"The `/status` command is only available to administrators.\\n\\n\"\n                                \"If you need access to system status information, please contact your system administrator.\",\n                                markdown=True,\n                            ),\n                            simple_component=None,\n                        )\n                    ],\n                )\n            return await self._generate_status_check(agent, user)\n\n        # Handle get recent memories command (admin-only)\n        if message.strip().lower() in [\n            \"/memories\",\n            \"memories\",\n            \"/recent_memories\",\n            \"recent_memories\",\n        ]:\n            # Check if user is admin\n            if \"admin\" not in user.group_memberships:\n                return WorkflowResult(\n                    should_skip_llm=True,\n                    components=[\n                        UiComponent(\n                            rich_component=RichTextComponent(\n                                content=\"# 🔒 Access Denied\\n\\n\"\n                                \"The `/memories` command is only available to administrators.\\n\\n\"\n                                \"If you need access to memory management features, please contact your system administrator.\",\n                                markdown=True,\n                            ),\n                            simple_component=None,\n                        )\n                    ],\n                )\n            return await self._get_recent_memories(agent, user, conversation)\n\n        # Handle delete memory command (admin-only)\n        if message.strip().lower().startswith(\"/delete \"):\n            # Check if user is admin\n            if \"admin\" not in user.group_memberships:\n                return WorkflowResult(\n                    should_skip_llm=True,\n                    components=[\n                        UiComponent(\n                            rich_component=RichTextComponent(\n                                content=\"# 🔒 Access Denied\\n\\n\"\n                                \"The `/delete` command is only available to administrators.\\n\\n\"\n                                \"If you need access to memory management features, please contact your system administrator.\",\n                                markdown=True,\n                            ),\n                            simple_component=None,\n                        )\n                    ],\n                )\n            memory_id = message.strip()[8:].strip()  # Extract ID after \"/delete \"\n            return await self._delete_memory(agent, user, conversation, memory_id)\n\n        # Don't handle other messages, pass to LLM\n        return WorkflowResult(should_skip_llm=False)\n\n    async def get_starter_ui(\n        self, agent: \"Agent\", user: \"User\", conversation: \"Conversation\"\n    ) -> Optional[List[UiComponent]]:\n        \"\"\"Generate starter UI based on available tools and setup status.\"\"\"\n\n        # Get available tools\n        tools = await agent.tool_registry.get_schemas(user)\n        tool_names = [tool.name for tool in tools]\n\n        # Analyze setup\n        setup_analysis = self._analyze_setup(tool_names)\n\n        # Check if user is admin (has 'admin' in group memberships)\n        is_admin = \"admin\" in user.group_memberships\n\n        # Generate single concise card\n        if self.welcome_message:\n            # Use custom welcome message\n            return [\n                UiComponent(\n                    rich_component=RichTextComponent(\n                        content=self.welcome_message, markdown=True\n                    ),\n                    simple_component=None,\n                )\n            ]\n        else:\n            # Generate role-aware welcome card\n            return [self._generate_starter_card(setup_analysis, is_admin)]\n\n    def _generate_starter_card(\n        self, analysis: Dict[str, Any], is_admin: bool\n    ) -> UiComponent:\n        \"\"\"Generate a single concise starter card based on role and setup status.\"\"\"\n\n        if is_admin:\n            # Admin view: includes setup status and memory management\n            return self._generate_admin_starter_card(analysis)\n        else:\n            # User view: simple welcome message\n            return self._generate_user_starter_card(analysis)\n\n    def _generate_admin_starter_card(self, analysis: Dict[str, Any]) -> UiComponent:\n        \"\"\"Generate admin starter card with setup info and memory management.\"\"\"\n\n        # Build concise content\n        if not analysis[\"has_sql\"]:\n            title = \"Admin: Setup Required\"\n            content = \"**🔒 Admin View** - You have admin privileges and will see additional system information.\\n\\n**Vanna AI** requires a SQL connection to function.\\n\\nPlease configure a SQL tool to get started.\"\n            status = \"error\"\n            icon = \"⚠️\"\n        elif analysis[\"is_complete\"]:\n            title = \"Admin: System Ready\"\n            content = \"**🔒 Admin View** - You have admin privileges and will see additional system information.\\n\\n**Vanna AI** is fully configured and ready.\\n\\n\"\n            content += \"**Setup:** SQL ✓ | Memory ✓ | Visualization ✓\"\n            status = \"success\"\n            icon = \"✅\"\n        else:\n            title = \"Admin: System Ready\"\n            content = \"**🔒 Admin View** - You have admin privileges and will see additional system information.\\n\\n**Vanna AI** is ready to query your database.\\n\\n\"\n            setup_items = []\n            setup_items.append(\"SQL ✓\")\n            setup_items.append(\"Memory ✓\" if analysis[\"has_memory\"] else \"Memory ✗\")\n            setup_items.append(\"Viz ✓\" if analysis[\"has_viz\"] else \"Viz ✗\")\n            content += f\"**Setup:** {' | '.join(setup_items)}\"\n            status = \"warning\" if not analysis[\"has_memory\"] else \"success\"\n            icon = \"⚠️\" if not analysis[\"has_memory\"] else \"✅\"\n\n        # Add memory management info for admins\n        actions: List[Dict[str, Any]] = []\n        if analysis[\"has_sql\"]:\n            actions.append(\n                {\n                    \"label\": \"💡 Help\",\n                    \"action\": \"/help\",\n                    \"variant\": \"secondary\",\n                }\n            )\n\n        if analysis[\"has_memory\"]:\n            content += \"\\n\\n**Memory Management:** Tool and text memories are available. As an admin, you can view and manage these memories to help me learn from successful queries.\"\n            actions.append(\n                {\n                    \"label\": \"🧠 View Memories\",\n                    \"action\": \"/memories\",\n                    \"variant\": \"secondary\",\n                }\n            )\n\n        return UiComponent(\n            rich_component=CardComponent(\n                title=title,\n                content=content,\n                icon=icon,\n                status=status,\n                actions=actions,\n                markdown=True,\n            ),\n            simple_component=None,\n        )\n\n    def _generate_user_starter_card(self, analysis: Dict[str, Any]) -> UiComponent:\n        \"\"\"Generate simple user starter view using RichTextComponent.\"\"\"\n\n        if not analysis[\"has_sql\"]:\n            content = (\n                \"# ⚠️ Setup Required\\n\\n\"\n                \"Vanna AI requires configuration before it can help you analyze data.\"\n            )\n        else:\n            content = (\n                \"# 👋 Welcome to Vanna AI\\n\\n\"\n                \"I'm your AI data analyst assistant. Ask me anything about your data in plain English!\\n\\n\"\n                \"Type `/help` to see what I can do.\"\n            )\n\n        return UiComponent(\n            rich_component=RichTextComponent(content=content, markdown=True),\n            simple_component=None,\n        )\n\n    def _analyze_setup(self, tool_names: List[str]) -> Dict[str, Any]:\n        \"\"\"Analyze the current tool setup and return status.\"\"\"\n\n        # Critical tools\n        has_sql = any(\n            name in tool_names\n            for name in [\"run_sql\", \"sql_query\", \"execute_sql\", \"query_sql\"]\n        )\n\n        # Memory tools (important but not critical)\n        has_search = \"search_saved_correct_tool_uses\" in tool_names\n        has_save = \"save_question_tool_args\" in tool_names\n        has_memory = has_search and has_save\n\n        # Visualization tools (nice to have)\n        has_viz = any(\n            name in tool_names\n            for name in [\n                \"visualize_data\",\n                \"create_chart\",\n                \"plot_data\",\n                \"generate_chart\",\n            ]\n        )\n\n        # Other useful tools\n        has_calculator = any(\n            name in tool_names for name in [\"calculator\", \"calc\", \"calculate\"]\n        )\n\n        # Determine overall status\n        is_complete = has_sql and has_memory and has_viz\n        is_functional = has_sql\n\n        return {\n            \"has_sql\": has_sql,\n            \"has_memory\": has_memory,\n            \"has_search\": has_search,\n            \"has_save\": has_save,\n            \"has_viz\": has_viz,\n            \"has_calculator\": has_calculator,\n            \"is_complete\": is_complete,\n            \"is_functional\": is_functional,\n            \"tool_count\": len(tool_names),\n            \"tool_names\": tool_names,\n        }\n\n    def _generate_setup_status_cards(\n        self, analysis: Dict[str, Any]\n    ) -> List[UiComponent]:\n        \"\"\"Generate status cards showing setup health (used by /status command).\"\"\"\n\n        cards = []\n\n        # SQL Tool Status (Critical)\n        if analysis[\"has_sql\"]:\n            sql_card = StatusCardComponent(\n                title=\"SQL Connection\",\n                status=\"success\",\n                description=\"Database connection configured and ready\",\n                icon=\"✅\",\n            )\n        else:\n            sql_card = StatusCardComponent(\n                title=\"SQL Connection\",\n                status=\"error\",\n                description=\"No SQL tool detected - this is required for data analysis\",\n                icon=\"❌\",\n            )\n        cards.append(UiComponent(rich_component=sql_card, simple_component=None))\n\n        # Memory Tools Status (Important)\n        if analysis[\"has_memory\"]:\n            memory_card = StatusCardComponent(\n                title=\"Memory System\",\n                status=\"success\",\n                description=\"Search and save tools configured - I can learn from successful queries\",\n                icon=\"🧠\",\n            )\n        elif analysis[\"has_search\"] or analysis[\"has_save\"]:\n            memory_card = StatusCardComponent(\n                title=\"Memory System\",\n                status=\"warning\",\n                description=\"Partial memory setup - both search and save tools recommended\",\n                icon=\"⚠️\",\n            )\n        else:\n            memory_card = StatusCardComponent(\n                title=\"Memory System\",\n                status=\"warning\",\n                description=\"Memory tools not configured - I won't remember successful patterns\",\n                icon=\"⚠️\",\n            )\n        cards.append(UiComponent(rich_component=memory_card, simple_component=None))\n\n        # Visualization Status (Nice to have)\n        if analysis[\"has_viz\"]:\n            viz_card = StatusCardComponent(\n                title=\"Visualization\",\n                status=\"success\",\n                description=\"Chart creation tools available\",\n                icon=\"📊\",\n            )\n        else:\n            viz_card = StatusCardComponent(\n                title=\"Visualization\",\n                status=\"info\",\n                description=\"No visualization tools - results will be text/tables only\",\n                icon=\"📋\",\n            )\n        cards.append(UiComponent(rich_component=viz_card, simple_component=None))\n\n        return cards\n\n    def _generate_setup_guidance(\n        self, analysis: Dict[str, Any]\n    ) -> Optional[UiComponent]:\n        \"\"\"Generate setup guidance based on what's missing (used by /status command).\"\"\"\n\n        if not analysis[\"has_sql\"]:\n            # Critical guidance - need SQL\n            content = (\n                \"## 🚨 Setup Required\\n\\n\"\n                \"To get started with Vanna AI, you need to configure a SQL connection tool:\\n\\n\"\n                \"```python\\n\"\n                \"from vanna.tools import RunSqlTool\\n\\n\"\n                \"# Add SQL tool to your agent\\n\"\n                \"tool_registry.register(RunSqlTool(\\n\"\n                '    connection_string=\"your-database-connection\"\\n'\n                \"))\\n\"\n                \"```\\n\\n\"\n                \"**Next Steps:**\\n\"\n                \"1. Configure your database connection\\n\"\n                \"2. Add memory tools for learning\\n\"\n                \"3. Add visualization tools for charts\"\n            )\n\n        else:\n            # Improvement suggestions\n            suggestions = []\n\n            if not analysis[\"has_memory\"]:\n                suggestions.append(\n                    \"**🧠 Add Memory Tools** - Help me learn from successful queries:\\n\"\n                    \"```python\\n\"\n                    \"from vanna.tools import SearchSavedCorrectToolUses, SaveQuestionToolArgs\\n\"\n                    \"tool_registry.register(SearchSavedCorrectToolUses())\\n\"\n                    \"tool_registry.register(SaveQuestionToolArgs())\\n\"\n                    \"```\"\n                )\n\n            if not analysis[\"has_viz\"]:\n                suggestions.append(\n                    \"**📊 Add Visualization** - Create charts and graphs:\\n\"\n                    \"```python\\n\"\n                    \"from vanna.tools import VisualizeDataTool\\n\"\n                    \"tool_registry.register(VisualizeDataTool())\\n\"\n                    \"```\"\n                )\n\n            if suggestions:\n                content = \"## 💡 Suggested Improvements\\n\\n\" + \"\\n\\n\".join(suggestions)\n            else:\n                return None  # No guidance needed\n\n        return UiComponent(\n            rich_component=RichTextComponent(content=content, markdown=True),\n            simple_component=None,\n        )\n\n    async def _generate_status_check(\n        self, agent: \"Agent\", user: \"User\"\n    ) -> WorkflowResult:\n        \"\"\"Generate a detailed status check response.\"\"\"\n\n        # Get available tools\n        tools = await agent.tool_registry.get_schemas(user)\n        tool_names = [tool.name for tool in tools]\n        analysis = self._analyze_setup(tool_names)\n\n        # Generate status report\n        status_content = \"# 🔍 Setup Status Report\\n\\n\"\n\n        if analysis[\"is_complete\"]:\n            status_content += (\n                \"🎉 **Excellent!** Your Vanna AI setup is complete and optimized.\\n\\n\"\n            )\n        elif analysis[\"is_functional\"]:\n            status_content += (\n                \"✅ **Good!** Your setup is functional with room for improvement.\\n\\n\"\n            )\n        else:\n            status_content += (\n                \"⚠️ **Action Required** - Your setup needs configuration.\\n\\n\"\n            )\n\n        status_content += f\"**Tools Detected:** {analysis['tool_count']} total\\n\\n\"\n\n        # Tool breakdown\n        status_content += \"## Tool Status\\n\\n\"\n        status_content += f\"- **SQL Connection:** {'✅ Available' if analysis['has_sql'] else '❌ Missing (Required)'}\\n\"\n        status_content += f\"- **Memory System:** {'✅ Complete' if analysis['has_memory'] else '⚠️ Incomplete' if analysis['has_search'] or analysis['has_save'] else '❌ Missing'}\\n\"\n        status_content += f\"- **Visualization:** {'✅ Available' if analysis['has_viz'] else '📋 Text/Tables Only'}\\n\"\n        status_content += f\"- **Calculator:** {'✅ Available' if analysis['has_calculator'] else '➖ Not Available'}\\n\\n\"\n\n        if analysis[\"tool_names\"]:\n            status_content += (\n                f\"**Available Tools:** {', '.join(sorted(analysis['tool_names']))}\"\n            )\n\n        components = [\n            UiComponent(\n                rich_component=RichTextComponent(content=status_content, markdown=True),\n                simple_component=None,\n            )\n        ]\n\n        # Add status cards\n        components.extend(self._generate_setup_status_cards(analysis))\n\n        # Add guidance if needed\n        guidance = self._generate_setup_guidance(analysis)\n        if guidance:\n            components.append(guidance)\n\n        return WorkflowResult(should_skip_llm=True, components=components)\n\n    async def _get_recent_memories(\n        self, agent: \"Agent\", user: \"User\", conversation: \"Conversation\"\n    ) -> WorkflowResult:\n        \"\"\"Get and display recent memories from agent memory.\"\"\"\n        try:\n            # Check if agent has memory capability\n            if not hasattr(agent, \"agent_memory\") or agent.agent_memory is None:\n                return WorkflowResult(\n                    should_skip_llm=True,\n                    components=[\n                        UiComponent(\n                            rich_component=RichTextComponent(\n                                content=\"# ⚠️ No Memory System\\n\\n\"\n                                \"Agent memory is not configured. Recent memories are not available.\\n\\n\"\n                                \"To enable memory, configure an AgentMemory implementation in your agent setup.\",\n                                markdown=True,\n                            ),\n                            simple_component=None,\n                        )\n                    ],\n                )\n\n            # Create tool context\n            from vanna.core.tool import ToolContext\n\n            context = ToolContext(\n                user=user,\n                conversation_id=conversation.id,\n                request_id=str(uuid.uuid4()),\n                agent_memory=agent.agent_memory,\n            )\n\n            # Get both tool memories and text memories\n            tool_memories = await agent.agent_memory.get_recent_memories(\n                context=context, limit=10\n            )\n\n            # Try to get text memories (may not be implemented in all memory backends)\n            text_memories = []\n            try:\n                text_memories = await agent.agent_memory.get_recent_text_memories(\n                    context=context, limit=10\n                )\n            except (AttributeError, NotImplementedError):\n                # Text memories not supported by this implementation\n                pass\n\n            if not tool_memories and not text_memories:\n                return WorkflowResult(\n                    should_skip_llm=True,\n                    components=[\n                        UiComponent(\n                            rich_component=RichTextComponent(\n                                content=\"# 🧠 Recent Memories\\n\\n\"\n                                \"No recent memories found. As you use tools and ask questions, \"\n                                \"successful patterns will be saved here for future reference.\",\n                                markdown=True,\n                            ),\n                            simple_component=None,\n                        )\n                    ],\n                )\n\n            components = []\n\n            # Header\n            total_count = len(tool_memories) + len(text_memories)\n            header_content = f\"# 🧠 Recent Memories\\n\\nFound {total_count} recent memor{'y' if total_count == 1 else 'ies'}\"\n            components.append(\n                UiComponent(\n                    rich_component=RichTextComponent(\n                        content=header_content, markdown=True\n                    ),\n                    simple_component=None,\n                )\n            )\n\n            # Display text memories\n            if text_memories:\n                components.append(\n                    UiComponent(\n                        rich_component=RichTextComponent(\n                            content=f\"## 📝 Text Memories ({len(text_memories)})\",\n                            markdown=True,\n                        ),\n                        simple_component=None,\n                    )\n                )\n\n                for memory in text_memories:\n                    # Create card with delete button\n                    card_content = f\"**Content:** {memory.content}\\n\\n\"\n                    if memory.timestamp:\n                        card_content += f\"**Timestamp:** {memory.timestamp}\\n\\n\"\n                    card_content += f\"**ID:** `{memory.memory_id}`\"\n\n                    card = CardComponent(\n                        title=\"Text Memory\",\n                        content=card_content,\n                        icon=\"📝\",\n                        actions=[\n                            {\n                                \"label\": \"🗑️ Delete\",\n                                \"action\": f\"/delete {memory.memory_id}\",\n                                \"variant\": \"error\",\n                            }\n                        ],\n                    )\n                    components.append(\n                        UiComponent(rich_component=card, simple_component=None)\n                    )\n\n            # Display tool memories\n            if tool_memories:\n                components.append(\n                    UiComponent(\n                        rich_component=RichTextComponent(\n                            content=f\"## 🔧 Tool Memories ({len(tool_memories)})\",\n                            markdown=True,\n                        ),\n                        simple_component=None,\n                    )\n                )\n\n                for tool_memory in tool_memories:\n                    # Create card with delete button\n                    card_content = f\"**Question:** {tool_memory.question}\\n\\n\"\n                    card_content += f\"**Tool:** {tool_memory.tool_name}\\n\\n\"\n                    card_content += f\"**Arguments:** `{tool_memory.args}`\\n\\n\"\n                    card_content += f\"**Success:** {'✅ Yes' if tool_memory.success else '❌ No'}\\n\\n\"\n                    if tool_memory.timestamp:\n                        card_content += f\"**Timestamp:** {tool_memory.timestamp}\\n\\n\"\n                    card_content += f\"**ID:** `{tool_memory.memory_id}`\"\n\n                    card = CardComponent(\n                        title=f\"Tool: {tool_memory.tool_name}\",\n                        content=card_content,\n                        markdown=True,\n                        icon=\"🔧\",\n                        status=\"success\" if tool_memory.success else \"error\",\n                        actions=[\n                            {\n                                \"label\": \"🗑️ Delete\",\n                                \"action\": f\"/delete {tool_memory.memory_id}\",\n                                \"variant\": \"error\",\n                            }\n                        ],\n                    )\n                    components.append(\n                        UiComponent(rich_component=card, simple_component=None)\n                    )\n\n            return WorkflowResult(should_skip_llm=True, components=components)\n\n        except Exception as e:\n            traceback.print_exc()\n            return WorkflowResult(\n                should_skip_llm=True,\n                components=[\n                    UiComponent(\n                        rich_component=RichTextComponent(\n                            content=f\"# ❌ Error Retrieving Memories\\n\\n\"\n                            f\"Failed to get recent memories: {str(e)}\\n\\n\"\n                            f\"This may indicate an issue with the agent memory configuration.\",\n                            markdown=True,\n                        ),\n                        simple_component=None,\n                    )\n                ],\n            )\n\n    async def _delete_memory(\n        self, agent: \"Agent\", user: \"User\", conversation: \"Conversation\", memory_id: str\n    ) -> WorkflowResult:\n        \"\"\"Delete a memory by its ID.\"\"\"\n        try:\n            # Check if agent has memory capability\n            if not hasattr(agent, \"agent_memory\") or agent.agent_memory is None:\n                return WorkflowResult(\n                    should_skip_llm=True,\n                    components=[\n                        UiComponent(\n                            rich_component=RichTextComponent(\n                                content=\"# ⚠️ No Memory System\\n\\n\"\n                                \"Agent memory is not configured. Cannot delete memories.\",\n                                markdown=True,\n                            ),\n                            simple_component=None,\n                        )\n                    ],\n                )\n\n            if not memory_id:\n                return WorkflowResult(\n                    should_skip_llm=True,\n                    components=[\n                        UiComponent(\n                            rich_component=RichTextComponent(\n                                content=\"# ⚠️ Invalid Command\\n\\n\"\n                                \"Please provide a memory ID to delete.\\n\\n\"\n                                \"Usage: `/delete [memory_id]`\",\n                                markdown=True,\n                            ),\n                            simple_component=None,\n                        )\n                    ],\n                )\n\n            # Create tool context\n            from vanna.core.tool import ToolContext\n\n            context = ToolContext(\n                user=user,\n                conversation_id=conversation.id,\n                request_id=str(uuid.uuid4()),\n                agent_memory=agent.agent_memory,\n            )\n\n            # Try to delete as a tool memory first\n            deleted = await agent.agent_memory.delete_by_id(context, memory_id)\n\n            # If not found as tool memory, try as text memory\n            if not deleted:\n                try:\n                    deleted = await agent.agent_memory.delete_text_memory(\n                        context, memory_id\n                    )\n                except (AttributeError, NotImplementedError):\n                    # Text memory deletion not supported by this implementation\n                    pass\n\n            if deleted:\n                return WorkflowResult(\n                    should_skip_llm=True,\n                    components=[\n                        UiComponent(\n                            rich_component=RichTextComponent(\n                                content=f\"# ✅ Memory Deleted\\n\\n\"\n                                f\"Successfully deleted memory with ID: `{memory_id}`\\n\\n\"\n                                f\"You can view remaining memories using `/memories`.\",\n                                markdown=True,\n                            ),\n                            simple_component=None,\n                        )\n                    ],\n                )\n            else:\n                return WorkflowResult(\n                    should_skip_llm=True,\n                    components=[\n                        UiComponent(\n                            rich_component=RichTextComponent(\n                                content=f\"# ❌ Memory Not Found\\n\\n\"\n                                f\"Could not find memory with ID: `{memory_id}`\\n\\n\"\n                                f\"Use `/memories` to see available memory IDs.\",\n                                markdown=True,\n                            ),\n                            simple_component=None,\n                        )\n                    ],\n                )\n\n        except Exception as e:\n            traceback.print_exc()\n            return WorkflowResult(\n                should_skip_llm=True,\n                components=[\n                    UiComponent(\n                        rich_component=RichTextComponent(\n                            content=f\"# ❌ Error Deleting Memory\\n\\n\"\n                            f\"Failed to delete memory: {str(e)}\\n\\n\"\n                            f\"This may indicate an issue with the agent memory configuration.\",\n                            markdown=True,\n                        ),\n                        simple_component=None,\n                    )\n                ],\n            )\n"
  },
  {
    "path": "src/vanna/examples/__init__.py",
    "content": "\"\"\"Examples for using the Vanna Agents framework.\"\"\"\n"
  },
  {
    "path": "src/vanna/examples/__main__.py",
    "content": "\"\"\"\nInteractive example runner for Vanna Agents.\n\"\"\"\n\nimport sys\nimport importlib\n\n\ndef main() -> None:\n    \"\"\"Run an example interactively.\"\"\"\n    if len(sys.argv) < 2:\n        print(\"Available examples:\")\n        print(\"  python -m vanna.examples mock_quickstart\")\n        print(\"  python -m vanna.examples mock_custom_tool\")\n        print(\"  python -m vanna.examples anthropic_quickstart\")\n        print(\"  python -m vanna.examples openai_quickstart\")\n        print(\"  python -m vanna.examples mock_quota_example\")\n        print(\"  python -m vanna.examples mock_rich_components_demo\")\n        print(\"\")\n        print(\"Usage: python -m vanna.examples <example_name>\")\n        return\n\n    example_name = sys.argv[1]\n    try:\n        module = importlib.import_module(f\"vanna.examples.{example_name}\")\n        if hasattr(module, \"run_interactive\"):\n            module.run_interactive()\n        elif hasattr(module, \"main\"):\n            import asyncio\n\n            if asyncio.iscoroutinefunction(module.main):\n                asyncio.run(module.main())\n            else:\n                module.main()\n        else:\n            print(f\"Example '{example_name}' does not have a main function\")\n    except ImportError:\n        print(f\"Example '{example_name}' not found\")\n    except Exception as e:\n        print(f\"Error running example '{example_name}': {e}\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "src/vanna/examples/anthropic_quickstart.py",
    "content": "\"\"\"\nAnthropic example using AnthropicLlmService.\n\nLoads environment from .env (via python-dotenv), uses model 'claude-sonnet-4-20250514'\nby default, and sends a simple message through a Agent.\n\nRun:\n  PYTHONPATH=. python vanna/examples/anthropic_quickstart.py\n\"\"\"\n\nimport asyncio\nimport importlib.util\nimport os\nimport sys\n\n\ndef ensure_env() -> None:\n    if importlib.util.find_spec(\"dotenv\") is not None:\n        from dotenv import load_dotenv\n\n        # Load from local .env without overriding existing env\n        load_dotenv(dotenv_path=os.path.join(os.getcwd(), \".env\"), override=False)\n    else:\n        print(\n            \"[warn] python-dotenv not installed; skipping .env load. Install with: pip install python-dotenv\"\n        )\n\n    if not os.getenv(\"ANTHROPIC_API_KEY\"):\n        print(\n            \"[error] ANTHROPIC_API_KEY is not set. Add it to your environment or .env file.\"\n        )\n        sys.exit(1)\n\n\nasync def main() -> None:\n    ensure_env()\n\n    try:\n        from vanna.integrations.anthropic import AnthropicLlmService\n    except ImportError:\n        print(\n            \"[error] anthropic extra not installed. Install with: pip install -e .[anthropic]\"\n        )\n        raise\n\n    from vanna import AgentConfig, Agent, User\n    from vanna.core.registry import ToolRegistry\n    from vanna.tools import ListFilesTool\n\n    model = os.getenv(\"ANTHROPIC_MODEL\", \"claude-sonnet-4-20250514\")\n    print(f\"Using Anthropic model: {model}\")\n\n    llm = AnthropicLlmService(model=model)\n\n    # Create tool registry and register the list_files tool\n    tool_registry = ToolRegistry()\n    list_files_tool = ListFilesTool()\n    tool_registry.register(list_files_tool)\n\n    agent = Agent(\n        llm_service=llm,\n        config=AgentConfig(stream_responses=False),\n        tool_registry=tool_registry,\n    )\n\n    user = User(id=\"demo-user\", username=\"demo\")\n    conversation_id = \"anthropic-demo\"\n\n    print(\"Sending: 'List the files in the current directory'\\n\")\n    async for component in agent.send_message(\n        user=user,\n        message=\"List the files in the current directory\",\n        conversation_id=conversation_id,\n    ):\n        if hasattr(component, \"content\") and component.content:\n            print(\"Assistant:\", component.content)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "src/vanna/examples/artifact_example.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nExample demonstrating the artifact system in Vanna Agents.\n\nThis script shows how agents can create interactive artifacts that can be\nrendered externally by developers listening for the 'artifact-opened' event.\n\"\"\"\n\nimport asyncio\nfrom typing import AsyncGenerator, Optional\n\nfrom vanna import Agent, UiComponent, User, AgentConfig\nfrom vanna.core.rich_components import ArtifactComponent\nfrom vanna.integrations.anthropic.mock import MockLlmService\nfrom vanna.core.interfaces import Agent, LlmService\n\n\nclass ArtifactDemoAgent(Agent):\n    \"\"\"Demo agent that creates various types of artifacts.\"\"\"\n\n    def __init__(self, llm_service: Optional[LlmService] = None) -> None:\n        if llm_service is None:\n            llm_service = MockLlmService(\n                \"I'll help you create interactive artifacts! Try asking me to create a chart, dashboard, or interactive HTML widget.\"\n            )\n        super().__init__(\n            llm_service=llm_service,\n            config=AgentConfig(\n                stream_responses=True,\n                include_thinking_indicators=True,\n            ),\n        )\n\n    async def send_message(\n        self, user: User, message: str, *, conversation_id: Optional[str] = None\n    ) -> AsyncGenerator[UiComponent, None]:\n        \"\"\"Handle user messages and create appropriate artifacts.\"\"\"\n        # First send the normal response\n        async for component in super().send_message(\n            user, message, conversation_id=conversation_id\n        ):\n            yield component\n\n        # Then create artifacts based on message content\n        message_lower = message.lower()\n\n        if any(\n            word in message_lower for word in [\"chart\", \"graph\", \"visualization\", \"d3\"]\n        ):\n            async for component in self.create_d3_visualization():\n                yield component\n        elif any(\n            word in message_lower for word in [\"dashboard\", \"analytics\", \"metrics\"]\n        ):\n            async for component in self.create_dashboard_artifact():\n                yield component\n        elif any(\n            word in message_lower for word in [\"html\", \"interactive\", \"widget\", \"demo\"]\n        ):\n            async for component in self.create_html_artifact():\n                yield component\n\n    async def create_html_artifact(self) -> AsyncGenerator[UiComponent, None]:\n        \"\"\"Create a simple HTML artifact.\"\"\"\n        html_content = \"\"\"\n        <div style=\"padding: 20px; font-family: Arial, sans-serif;\">\n            <h2 style=\"color: #333;\">Interactive HTML Artifact</h2>\n            <p>This is a simple HTML artifact that can be opened externally.</p>\n            <button onclick=\"alert('Hello from the artifact!')\">Click me!</button>\n            <div style=\"margin-top: 20px;\">\n                <input type=\"text\" placeholder=\"Type something...\" id=\"textInput\">\n                <button onclick=\"document.getElementById('output').textContent = document.getElementById('textInput').value\">\n                    Update Text\n                </button>\n            </div>\n            <div id=\"output\" style=\"margin-top: 10px; padding: 10px; background: #f0f0f0; border-radius: 4px;\">\n                Output will appear here...\n            </div>\n        </div>\n        \"\"\"\n\n        artifact = ArtifactComponent.create_html(\n            content=html_content,\n            title=\"Interactive HTML Demo\",\n            description=\"A simple HTML artifact with interactive elements\",\n        )\n\n        yield UiComponent(rich_component=artifact)\n\n    async def create_d3_visualization(self) -> AsyncGenerator[UiComponent, None]:\n        \"\"\"Create a D3.js visualization artifact.\"\"\"\n        d3_content = \"\"\"\n        <div id=\"chart\" style=\"width: 100%; height: 400px;\"></div>\n        <script>\n            // Sample data\n            const data = [\n                {name: 'A', value: 30},\n                {name: 'B', value: 80},\n                {name: 'C', value: 45},\n                {name: 'D', value: 60},\n                {name: 'E', value: 20},\n                {name: 'F', value: 90}\n            ];\n\n            // Set up dimensions\n            const margin = {top: 20, right: 30, bottom: 40, left: 40};\n            const width = 800 - margin.left - margin.right;\n            const height = 400 - margin.top - margin.bottom;\n\n            // Create SVG\n            const svg = d3.select(\"#chart\")\n                .append(\"svg\")\n                .attr(\"width\", width + margin.left + margin.right)\n                .attr(\"height\", height + margin.top + margin.bottom)\n                .append(\"g\")\n                .attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n            // Create scales\n            const xScale = d3.scaleBand()\n                .domain(data.map(d => d.name))\n                .range([0, width])\n                .padding(0.1);\n\n            const yScale = d3.scaleLinear()\n                .domain([0, d3.max(data, d => d.value)])\n                .range([height, 0]);\n\n            // Create bars\n            svg.selectAll(\".bar\")\n                .data(data)\n                .enter().append(\"rect\")\n                .attr(\"class\", \"bar\")\n                .attr(\"x\", d => xScale(d.name))\n                .attr(\"width\", xScale.bandwidth())\n                .attr(\"y\", d => yScale(d.value))\n                .attr(\"height\", d => height - yScale(d.value))\n                .attr(\"fill\", \"#4CAF50\")\n                .on(\"mouseover\", function(event, d) {\n                    d3.select(this).attr(\"fill\", \"#45a049\");\n                })\n                .on(\"mouseout\", function(event, d) {\n                    d3.select(this).attr(\"fill\", \"#4CAF50\");\n                });\n\n            // Add axes\n            svg.append(\"g\")\n                .attr(\"transform\", `translate(0,${height})`)\n                .call(d3.axisBottom(xScale));\n\n            svg.append(\"g\")\n                .call(d3.axisLeft(yScale));\n        </script>\n        \"\"\"\n\n        artifact = ArtifactComponent.create_d3(\n            content=d3_content,\n            title=\"D3.js Bar Chart\",\n            description=\"An interactive bar chart built with D3.js\",\n        )\n\n        yield UiComponent(rich_component=artifact)\n\n    async def create_dashboard_artifact(self) -> AsyncGenerator[UiComponent, None]:\n        \"\"\"Create a dashboard-style artifact.\"\"\"\n        dashboard_content = \"\"\"\n        <div style=\"padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; min-height: 100vh;\">\n            <h1 style=\"color: #333; margin-bottom: 30px;\">Analytics Dashboard</h1>\n\n            <div style=\"display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 30px;\">\n                <div class=\"metric-card\" style=\"background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);\">\n                    <h3 style=\"margin: 0; color: #666;\">Total Users</h3>\n                    <p style=\"font-size: 2em; font-weight: bold; color: #333; margin: 10px 0;\">12,456</p>\n                    <span style=\"color: #4CAF50;\">↗ +5.2%</span>\n                </div>\n\n                <div class=\"metric-card\" style=\"background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);\">\n                    <h3 style=\"margin: 0; color: #666;\">Revenue</h3>\n                    <p style=\"font-size: 2em; font-weight: bold; color: #333; margin: 10px 0;\">$89,432</p>\n                    <span style=\"color: #4CAF50;\">↗ +12.3%</span>\n                </div>\n\n                <div class=\"metric-card\" style=\"background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);\">\n                    <h3 style=\"margin: 0; color: #666;\">Conversion Rate</h3>\n                    <p style=\"font-size: 2em; font-weight: bold; color: #333; margin: 10px 0;\">3.4%</p>\n                    <span style=\"color: #f44336;\">↘ -0.8%</span>\n                </div>\n            </div>\n\n            <div style=\"background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);\">\n                <h3 style=\"margin: 0 0 20px 0; color: #333;\">Quick Actions</h3>\n                <div style=\"display: flex; gap: 10px; flex-wrap: wrap;\">\n                    <button onclick=\"alert('Export feature coming soon!')\"\n                            style=\"padding: 10px 20px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer;\">\n                        Export Data\n                    </button>\n                    <button onclick=\"alert('Refresh complete!')\"\n                            style=\"padding: 10px 20px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;\">\n                        Refresh\n                    </button>\n                    <button onclick=\"alert('Settings panel opened!')\"\n                            style=\"padding: 10px 20px; background: #FF9800; color: white; border: none; border-radius: 4px; cursor: pointer;\">\n                        Settings\n                    </button>\n                </div>\n            </div>\n        </div>\n        \"\"\"\n\n        artifact = ArtifactComponent(\n            content=dashboard_content,\n            artifact_type=\"dashboard\",\n            title=\"Analytics Dashboard\",\n            description=\"A sample analytics dashboard with metrics and controls\",\n            external_renderable=True,\n            fullscreen_capable=True,\n        )\n\n        yield UiComponent(rich_component=artifact)\n\n\ndef create_demo_agent() -> ArtifactDemoAgent:\n    \"\"\"Create a demo agent for REPL and server usage.\n\n    Returns:\n        Configured ArtifactDemoAgent instance\n    \"\"\"\n    return ArtifactDemoAgent()\n\n\nasync def main() -> None:\n    \"\"\"Main demo function.\"\"\"\n    print(\"🎨 Artifact Demo Agent\")\n    print(\"This demo shows how to create different types of artifacts.\")\n    print(\n        \"In a real web application, developers can listen for 'artifact-opened' events.\"\n    )\n    print()\n\n    demo_agent = create_demo_agent()\n    user = User(id=\"demo_user\", username=\"artifact_demo\")\n\n    # Demo 1: HTML Artifact\n    print(\"1. Creating HTML Artifact...\")\n    async for component in demo_agent.create_html_artifact():\n        artifact = component.rich_component\n        if isinstance(artifact, ArtifactComponent):\n            print(f\"   ✓ Created HTML artifact: {artifact.title}\")\n            print(f\"   ✓ Artifact ID: {artifact.artifact_id}\")\n            print(f\"   ✓ Type: {artifact.artifact_type}\")\n            print(f\"   ✓ External renderable: {artifact.external_renderable}\")\n        print()\n\n    # Demo 2: D3 Visualization\n    print(\"2. Creating D3.js Visualization...\")\n    async for component in demo_agent.create_d3_visualization():\n        artifact = component.rich_component\n        if isinstance(artifact, ArtifactComponent):\n            print(f\"   ✓ Created D3 artifact: {artifact.title}\")\n            print(f\"   ✓ Dependencies: {artifact.dependencies}\")\n            print(f\"   ✓ Standalone HTML available via get_standalone_html()\")\n        print()\n\n    # Demo 3: Dashboard\n    print(\"3. Creating Dashboard Artifact...\")\n    async for component in demo_agent.create_dashboard_artifact():\n        artifact = component.rich_component\n        if isinstance(artifact, ArtifactComponent):\n            print(f\"   ✓ Created dashboard artifact: {artifact.title}\")\n            print(f\"   ✓ Fullscreen capable: {artifact.fullscreen_capable}\")\n        print()\n\n    print(\"🚀 Web Integration Example:\")\n    print(\"\"\"\n    In your web application, listen for the 'artifact-opened' event:\n\n    document.querySelector('vanna-chat').addEventListener('artifact-opened', (event) => {\n        const { artifactId, content, type, trigger } = event.detail;\n\n        if (trigger === 'created' && type === 'dashboard') {\n            // Auto-open dashboards in external window\n            const newWindow = window.open('', '_blank');\n            newWindow.document.write(event.detail.getStandaloneHTML());\n            newWindow.document.close();\n\n            // Prevent default rendering in chat\n            event.detail.preventDefault();\n        }\n    });\n    \"\"\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "src/vanna/examples/claude_sqlite_example.py",
    "content": "\"\"\"\nClaude example using the SQL query tool with the Chinook database.\n\nThis example demonstrates using the RunSqlTool with SqliteRunner and Claude's AI\nto intelligently query and analyze the Chinook database, with automatic visualization support.\n\nRequirements:\n- ANTHROPIC_API_KEY environment variable or .env file\n- anthropic package: pip install -e .[anthropic]\n- plotly package: pip install -e .[visualization]\n\nUsage:\n  PYTHONPATH=. python vanna/examples/claude_sqlite_example.py\n\"\"\"\n\nimport asyncio\nimport importlib.util\nimport os\nimport sys\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from vanna import Agent\n\n\ndef ensure_env() -> None:\n    if importlib.util.find_spec(\"dotenv\") is not None:\n        from dotenv import load_dotenv\n\n        # Load from local .env without overriding existing env\n        load_dotenv(dotenv_path=os.path.join(os.getcwd(), \".env\"), override=False)\n    else:\n        print(\n            \"[warn] python-dotenv not installed; skipping .env load. Install with: pip install python-dotenv\"\n        )\n\n    if not os.getenv(\"ANTHROPIC_API_KEY\"):\n        print(\n            \"[error] ANTHROPIC_API_KEY is not set. Add it to your environment or .env file.\"\n        )\n        sys.exit(1)\n\n\nasync def main() -> None:\n    ensure_env()\n\n    try:\n        from vanna.integrations.anthropic import AnthropicLlmService\n    except ImportError:\n        print(\n            \"[error] anthropic extra not installed. Install with: pip install -e .[anthropic]\"\n        )\n        raise\n\n    from vanna import AgentConfig, Agent\n    from vanna.core.registry import ToolRegistry\n    from vanna.core.user import CookieEmailUserResolver, RequestContext\n    from vanna.integrations.sqlite import SqliteRunner\n    from vanna.tools import (\n        RunSqlTool,\n        VisualizeDataTool,\n        LocalFileSystem,\n    )\n\n    # Get the path to the Chinook database\n    database_path = os.path.join(\n        os.path.dirname(__file__), \"..\", \"..\", \"Chinook.sqlite\"\n    )\n    database_path = os.path.abspath(database_path)\n\n    if not os.path.exists(database_path):\n        print(f\"[error] Chinook database not found at {database_path}\")\n        print(\n            \"Please download it with: curl -o Chinook.sqlite https://vanna.ai/Chinook.sqlite\"\n        )\n        sys.exit(1)\n\n    model = os.getenv(\"ANTHROPIC_MODEL\", \"claude-sonnet-4-20250514\")\n    print(f\"Using Anthropic model: {model}\")\n    print(f\"Using database: {database_path}\")\n\n    llm = AnthropicLlmService(model=model)\n\n    # Create shared FileSystem for both tools\n    file_system = LocalFileSystem(working_directory=\"./claude_data\")\n\n    # Create tool registry and register the SQL tool with SQLite runner\n    tool_registry = ToolRegistry()\n    sqlite_runner = SqliteRunner(database_path=database_path)\n    sql_tool = RunSqlTool(sql_runner=sqlite_runner, file_system=file_system)\n    tool_registry.register(sql_tool)\n\n    # Register visualization tool\n    try:\n        viz_tool = VisualizeDataTool(file_system=file_system)\n        tool_registry.register(viz_tool)\n        print(\"Visualization tool enabled\")\n    except ImportError:\n        print(\n            \"[warn] Plotly not installed. Visualization tool disabled. Install with: pip install -e .[visualization]\"\n        )\n\n    user_resolver = CookieEmailUserResolver()\n\n    agent = Agent(\n        llm_service=llm,\n        config=AgentConfig(stream_responses=False),\n        tool_registry=tool_registry,\n        user_resolver=user_resolver,\n    )\n\n    # Simulate a logged-in demo user via cookie-based resolver\n    request_context = RequestContext(\n        cookies={user_resolver.cookie_name: \"demo-user@example.com\"},\n        metadata={\"demo\": True},\n        remote_addr=\"127.0.0.1\",\n    )\n    conversation_id = \"claude-sqlite-demo\"\n\n    # Sample queries to demonstrate different capabilities\n    sample_questions = [\n        \"What tables are in this database?\",\n        \"Show me the first 5 customers with their names\",\n        \"What's the total number of tracks in the database?\",\n        \"Find the top 5 artists by number of albums\",\n        \"What's the average invoice total?\",\n        \"Get data on the top 10 longest tracks and then visualize it\",\n    ]\n\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Claude SQLite Database Assistant Demo\")\n    print(\"=\" * 60)\n    print(\"This demo shows Claude querying the Chinook music database.\")\n    print(\"Claude will intelligently construct SQL queries to answer questions\")\n    print(\"and can create visualizations of the results.\")\n    print()\n\n    for i, question in enumerate(sample_questions, 1):\n        print(f\"\\n--- Question {i}: {question} ---\")\n\n        async for component in agent.send_message(\n            request_context=request_context,\n            message=question,\n            conversation_id=conversation_id,\n        ):\n            # Handle different component types\n            if hasattr(component, \"simple_component\") and component.simple_component:\n                if hasattr(component.simple_component, \"text\"):\n                    print(\"Assistant:\", component.simple_component.text)\n            elif hasattr(component, \"rich_component\") and component.rich_component:\n                if (\n                    hasattr(component.rich_component, \"content\")\n                    and component.rich_component.content\n                ):\n                    print(\"Assistant:\", component.rich_component.content)\n            elif hasattr(component, \"content\") and component.content:\n                print(\"Assistant:\", component.content)\n\n        print()  # Add spacing between questions\n\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Demo complete! Claude successfully queried the database.\")\n    print(\"=\" * 60)\n\n\ndef create_demo_agent() -> \"Agent\":\n    \"\"\"Create a demo agent with Claude and SQLite query tool.\n\n    This function is called by the vanna server framework.\n\n    Returns:\n        Configured Agent with Claude LLM and SQLite tool\n    \"\"\"\n    ensure_env()\n\n    try:\n        from vanna.integrations.anthropic import AnthropicLlmService\n    except ImportError:\n        print(\n            \"[error] anthropic extra not installed. Install with: pip install -e .[anthropic]\"\n        )\n        raise\n\n    from vanna import AgentConfig, Agent\n    from vanna.core.registry import ToolRegistry\n    from vanna.core.user import CookieEmailUserResolver\n    from vanna.integrations.sqlite import SqliteRunner\n    from vanna.tools import (\n        RunSqlTool,\n        VisualizeDataTool,\n        LocalFileSystem,\n    )\n\n    # Get the path to the Chinook database\n    database_path = os.path.join(\n        os.path.dirname(__file__), \"..\", \"..\", \"Chinook.sqlite\"\n    )\n    database_path = os.path.abspath(database_path)\n\n    if not os.path.exists(database_path):\n        raise FileNotFoundError(\n            f\"Chinook database not found at {database_path}. Please download it from https://vanna.ai/Chinook.sqlite\"\n        )\n\n    model = os.getenv(\"ANTHROPIC_MODEL\", \"claude-sonnet-4-20250514\")\n\n    llm = AnthropicLlmService(model=model)\n\n    # Create shared FileSystem for both tools\n    file_system = LocalFileSystem(working_directory=\"./claude_data\")\n\n    # Create tool registry and register the SQL tool with SQLite runner\n    tool_registry = ToolRegistry()\n    sqlite_runner = SqliteRunner(database_path=database_path)\n    sql_tool = RunSqlTool(sql_runner=sqlite_runner, file_system=file_system)\n    tool_registry.register(sql_tool)\n\n    # Register visualization tool if available\n    try:\n        viz_tool = VisualizeDataTool(file_system=file_system)\n        tool_registry.register(viz_tool)\n    except ImportError:\n        pass  # Visualization tool not available\n\n    user_resolver = CookieEmailUserResolver()\n\n    return Agent(\n        llm_service=llm,\n        config=AgentConfig(stream_responses=True),  # Enable streaming for web interface\n        tool_registry=tool_registry,\n        user_resolver=user_resolver,\n    )\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "src/vanna/examples/coding_agent_example.py",
    "content": "\"\"\"\nExample coding agent using the vanna-agents framework.\n\nThis example demonstrates building an agent that can edit code files,\nfollowing the concepts from the \"How to Build an Agent\" article.\nThe agent includes tools for file operations and uses an LLM service\nthat can understand and modify code.\n\nUsage:\n  PYTHONPATH=. python vanna/examples/coding_agent_example.py\n\"\"\"\n\nimport asyncio\nimport uuid\nfrom typing import AsyncGenerator, List, Optional\n\nfrom vanna import (\n    AgentConfig,\n    Agent,\n    ToolRegistry,\n    User,\n)\nfrom vanna.core.interfaces import LlmService\nfrom vanna.core.models import (\n    LlmRequest,\n    LlmResponse,\n    LlmStreamChunk,\n    ToolCall,\n    ToolSchema,\n)\nfrom vanna.tools.file_system import create_file_system_tools\nfrom vanna.tools.python import create_python_tools\n\n\nclass CodingLlmService(LlmService):\n    \"\"\"\n    LLM service that simulates a coding assistant.\n\n    This demonstrates the minimal implementation needed for an agent\n    as described in the article - just needs to understand tool calls\n    and respond appropriately.\n    \"\"\"\n\n    async def send_request(self, request: LlmRequest) -> LlmResponse:\n        \"\"\"Handle non-streaming requests.\"\"\"\n        await asyncio.sleep(0.1)  # Simulate thinking time\n        return self._build_response(request)\n\n    async def stream_request(\n        self, request: LlmRequest\n    ) -> AsyncGenerator[LlmStreamChunk, None]:\n        \"\"\"Handle streaming requests.\"\"\"\n        await asyncio.sleep(0.1)\n        response = self._build_response(request)\n\n        if response.tool_calls:\n            yield LlmStreamChunk(tool_calls=response.tool_calls)\n        if response.content:\n            # Simulate streaming by chunking the response\n            words = response.content.split()\n            for i, word in enumerate(words):\n                chunk = word if i == 0 else f\" {word}\"\n                await asyncio.sleep(0.05)  # Simulate streaming delay\n                yield LlmStreamChunk(content=chunk)\n\n        yield LlmStreamChunk(finish_reason=response.finish_reason)\n\n    async def validate_tools(self, tools: List[ToolSchema]) -> List[str]:\n        \"\"\"Validate tools - no errors for this simple implementation.\"\"\"\n        return []\n\n    def _build_response(self, request: LlmRequest) -> LlmResponse:\n        \"\"\"Build a response based on the conversation context.\"\"\"\n        last_message = request.messages[-1] if request.messages else None\n\n        # If we just got a tool result, respond to it\n        if last_message and last_message.role == \"tool\":\n            tool_result = last_message.content or \"Tool executed\"\n            return LlmResponse(\n                content=f\"I've completed the operation. {tool_result}\",\n                finish_reason=\"stop\",\n            )\n\n        # If user is asking for file operations, use tools\n        if last_message and last_message.role == \"user\":\n            user_message = last_message.content.lower()\n\n            if \"list files\" in user_message or \"show files\" in user_message:\n                return LlmResponse(\n                    content=\"I'll list the files for you.\",\n                    tool_calls=[\n                        ToolCall(\n                            id=f\"call_{uuid.uuid4().hex[:8]}\",\n                            name=\"list_files\",\n                            arguments={},\n                        )\n                    ],\n                    finish_reason=\"tool_calls\",\n                )\n\n            elif \"read\" in user_message and (\n                \"file\" in user_message\n                or \".py\" in user_message\n                or \".txt\" in user_message\n            ):\n                filename = _extract_filename(user_message)\n\n                if filename:\n                    return LlmResponse(\n                        content=f\"I'll read the file '{filename}' for you.\",\n                        tool_calls=[\n                            ToolCall(\n                                id=f\"call_{uuid.uuid4().hex[:8]}\",\n                                name=\"read_file\",\n                                arguments={\"filename\": filename},\n                            )\n                        ],\n                        finish_reason=\"tool_calls\",\n                    )\n\n            elif \"create\" in user_message or \"write\" in user_message:\n                # Suggest creating a simple example file\n                return LlmResponse(\n                    content=\"I'll create an example Python file for you.\",\n                    tool_calls=[\n                        ToolCall(\n                            id=f\"call_{uuid.uuid4().hex[:8]}\",\n                            name=\"write_file\",\n                            arguments={\n                                \"filename\": \"example.py\",\n                                \"content\": \"# Example Python file\\nprint('Hello from the coding agent!')\\n\\ndef greet(name):\\n    return f'Hello, {name}!'\\n\\nif __name__ == '__main__':\\n    print(greet('World'))\\n\",\n                                \"overwrite\": True,\n                            },\n                        )\n                    ],\n                    finish_reason=\"tool_calls\",\n                )\n\n            elif (\n                \"run\" in user_message or \"execute\" in user_message\n            ) and \".py\" in user_message:\n                filename = _extract_filename(user_message)\n                if filename:\n                    return LlmResponse(\n                        content=f\"I'll run the Python file '{filename}'.\",\n                        tool_calls=[\n                            ToolCall(\n                                id=f\"call_{uuid.uuid4().hex[:8]}\",\n                                name=\"run_python_file\",\n                                arguments={\n                                    \"filename\": filename,\n                                    \"arguments\": [],\n                                },\n                            )\n                        ],\n                        finish_reason=\"tool_calls\",\n                    )\n\n            elif (\n                \"edit\" in user_message\n                or \"update\" in user_message\n                or \"modify\" in user_message\n            ):\n                return LlmResponse(\n                    content=\"I'll update the greet function to make it more descriptive.\",\n                    tool_calls=[\n                        ToolCall(\n                            id=f\"call_{uuid.uuid4().hex[:8]}\",\n                            name=\"edit_file\",\n                            arguments={\n                                \"filename\": \"example.py\",\n                                \"edits\": [\n                                    {\n                                        \"start_line\": 4,\n                                        \"end_line\": 5,\n                                        \"new_content\": (\n                                            \"def greet(name):\\n\"\n                                            '    \"\"\"Return a friendly greeting.\"\"\"\\n'\n                                            '    return f\"Hello, {name}! Welcome to the coding agent.\"\\n'\n                                        ),\n                                    }\n                                ],\n                            },\n                        )\n                    ],\n                    finish_reason=\"tool_calls\",\n                )\n\n        # Default response\n        return LlmResponse(\n            content=(\n                \"I'm a coding assistant. I can help you list, read, write, edit, and run Python files. \"\n                \"Try asking me to 'list files', 'read example.py', 'create a Python file', 'run example.py', or 'update example.py'.\"\n            ),\n            finish_reason=\"stop\",\n        )\n\n\ndef create_demo_agent() -> Agent:\n    \"\"\"\n    Create a coding agent with file operation tools.\n\n    This follows the pattern from the article - minimal code\n    to create a powerful code-editing agent. Uses dependency injection\n    for file system operations with LocalFileSystem as default.\n    \"\"\"\n    # Create tool registry and register file system tools\n    tool_registry = ToolRegistry()\n\n    # Use the convenience function to create tools with default LocalFileSystem\n    for tool in create_file_system_tools():\n        tool_registry.register(tool)\n\n    for tool in create_python_tools():\n        tool_registry.register(tool)\n\n    # Create LLM service\n    llm_service = CodingLlmService()\n\n    # Create agent with configuration\n    return Agent(\n        llm_service=llm_service,\n        tool_registry=tool_registry,\n        config=AgentConfig(\n            stream_responses=True,\n            include_thinking_indicators=True,\n            max_tool_iterations=3,\n        ),\n    )\n\n\nasync def main() -> None:\n    \"\"\"\n    Demonstrate the coding agent in action.\n\n    As the article mentions: \"300 lines of code and three tools and now\n    you're able to talk to an alien intelligence that edits your code.\"\n    \"\"\"\n    print(\"🤖 Starting Coding Agent Demo\")\n    print(\"This demonstrates the concepts from 'How to Build an Agent'\")\n    print(\"-\" * 50)\n\n    # Create the agent\n    agent = create_demo_agent()\n\n    # Create a test user\n    user = User(id=\"coder123\", username=\"developer\", permissions=[])\n\n    # Show available tools\n    tools = await agent.get_available_tools(user)\n    print(f\"Available tools: {[tool.name for tool in tools]}\")\n    print()\n\n    # Demo conversation\n    conversation_id = \"coding-session\"\n\n    demos = [\n        \"Hello! Can you list the files in this directory?\",\n        \"Can you create a simple Python file for me?\",\n        \"Now read the example.py file you just created\",\n        \"Please update the greet function to include a docstring and a friendlier message.\",\n        \"Run example.py so I can see its output.\",\n        \"Great, read example.py again to confirm the changes.\",\n    ]\n\n    for i, message in enumerate(demos, 1):\n        print(f\"Demo {i}: {message}\")\n        print(\"Agent response:\")\n\n        async for component in agent.send_message(\n            user=user, message=message, conversation_id=conversation_id\n        ):\n            if (\n                hasattr(component.rich_component, \"content\")\n                and component.rich_component.content\n            ):\n                print(f\"  📝 {component.rich_component.content}\")\n            elif hasattr(component.rich_component, \"message\"):\n                print(f\"  💬 {component.rich_component.message}\")\n            elif component.simple_component and hasattr(\n                component.simple_component, \"text\"\n            ):\n                print(f\"  📄 {component.simple_component.text}\")\n\n        print(\"-\" * 30)\n\n\ndef _extract_filename(message: str) -> Optional[str]:\n    \"\"\"Extract a likely filename token from a user message.\"\"\"\n\n    for token in message.replace(\"\\n\", \" \").split():\n        cleaned = token.strip(\"'\\\".,;!?\")\n        if \".\" in cleaned and not cleaned.startswith(\".\"):\n            return cleaned\n\n    return None\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "src/vanna/examples/custom_system_prompt_example.py",
    "content": "\"\"\"\nExample demonstrating custom system prompt builder with dependency injection.\n\nThis example shows how to create a custom SystemPromptBuilder that dynamically\ngenerates system prompts based on user context and available tools.\n\nUsage:\n  python -m vanna.examples.custom_system_prompt_example\n\"\"\"\n\nfrom typing import List, Optional\n\nfrom vanna.core.interfaces import SystemPromptBuilder\nfrom vanna.core.models import ToolSchema, User\n\n\nclass CustomSystemPromptBuilder(SystemPromptBuilder):\n    \"\"\"Custom system prompt builder that personalizes prompts based on user.\"\"\"\n\n    async def build_system_prompt(\n        self, user: User, tools: List[ToolSchema]\n    ) -> Optional[str]:\n        \"\"\"Build a personalized system prompt.\n\n        Args:\n            user: The user making the request\n            tools: List of tools available to the user\n\n        Returns:\n            Personalized system prompt\n        \"\"\"\n        # Build personalized greeting\n        username = user.username or user.id\n        greeting = f\"Hello {username}! I'm your AI assistant.\"\n\n        # Add role-specific instructions based on user permissions\n        role_instructions = []\n        if \"admin\" in user.permissions:\n            role_instructions.append(\n                \"As an admin user, you have access to all tools and capabilities.\"\n            )\n        elif \"analyst\" in user.permissions:\n            role_instructions.append(\n                \"You're working as an analyst. I'll help you query and visualize data.\"\n            )\n        else:\n            role_instructions.append(\"I'm here to help you with your tasks.\")\n\n        # List available tools\n        tool_info = []\n        if tools:\n            tool_info.append(\"\\nAvailable tools:\")\n            for tool in tools:\n                tool_info.append(f\"- {tool.name}: {tool.description}\")\n\n        # Combine all parts\n        parts = [greeting] + role_instructions + tool_info\n        return \"\\n\".join(parts)\n\n\nclass SQLAssistantSystemPromptBuilder(SystemPromptBuilder):\n    \"\"\"System prompt builder specifically for SQL database assistants.\"\"\"\n\n    def __init__(self, database_name: str = \"database\"):\n        \"\"\"Initialize with database context.\n\n        Args:\n            database_name: Name of the database being queried\n        \"\"\"\n        self.database_name = database_name\n\n    async def build_system_prompt(\n        self, user: User, tools: List[ToolSchema]\n    ) -> Optional[str]:\n        \"\"\"Build a SQL-focused system prompt.\n\n        Args:\n            user: The user making the request\n            tools: List of tools available to the user\n\n        Returns:\n            SQL-focused system prompt\n        \"\"\"\n        prompt = f\"\"\"You are an expert SQL database assistant for the {self.database_name} database.\n\nYour primary responsibilities:\n1. Write efficient, correct SQL queries\n2. Explain query results clearly\n3. Suggest optimizations when relevant\n4. Visualize data when appropriate\n\nGuidelines:\n- Always validate SQL syntax before execution\n- Use appropriate JOINs and avoid Cartesian products\n- Limit result sets to reasonable sizes by default\n- Format numbers and dates for readability\n\"\"\"\n\n        # Add tool-specific instructions\n        has_viz_tool = any(tool.name == \"visualize_data\" for tool in tools)\n        if has_viz_tool:\n            prompt += \"\\n- Create visualizations for numerical data when it helps understanding\"\n\n        return prompt\n\n\nasync def demo() -> None:\n    \"\"\"Demonstrate custom system prompt builders.\"\"\"\n    from vanna import Agent, User\n    from vanna.core.registry import ToolRegistry\n    from vanna.integrations.anthropic.mock import MockLlmService\n\n    # Example 1: Custom personalized system prompt\n    print(\"=\" * 60)\n    print(\"Example 1: Custom Personalized System Prompt\")\n    print(\"=\" * 60)\n\n    custom_builder = CustomSystemPromptBuilder()\n    admin_user = User(id=\"user-1\", username=\"Alice\", permissions=[\"admin\"])\n\n    # Simulate some tools\n    mock_tools = [\n        ToolSchema(\n            name=\"query_database\", description=\"Query the SQL database\", parameters={}\n        ),\n        ToolSchema(\n            name=\"visualize_data\",\n            description=\"Create data visualizations\",\n            parameters={},\n        ),\n    ]\n\n    prompt = await custom_builder.build_system_prompt(admin_user, mock_tools)\n    print(\"\\nGenerated system prompt for admin user:\")\n    print(\"-\" * 60)\n    print(prompt)\n    print(\"-\" * 60)\n\n    # Example 2: SQL-specific system prompt\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Example 2: SQL Assistant System Prompt\")\n    print(\"=\" * 60)\n\n    sql_builder = SQLAssistantSystemPromptBuilder(database_name=\"Chinook\")\n    analyst_user = User(id=\"user-2\", username=\"Bob\", permissions=[\"analyst\"])\n\n    prompt = await sql_builder.build_system_prompt(analyst_user, mock_tools)\n    print(\"\\nGenerated system prompt for SQL assistant:\")\n    print(\"-\" * 60)\n    print(prompt)\n    print(\"-\" * 60)\n\n    # Example 3: Using custom builder with Agent\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Example 3: Using Custom Builder with Agent\")\n    print(\"=\" * 60)\n\n    mock_llm = MockLlmService()\n    tool_registry = ToolRegistry()\n\n    agent = Agent(\n        llm_service=mock_llm,\n        tool_registry=tool_registry,\n        system_prompt_builder=sql_builder,  # Inject custom builder here\n    )\n\n    print(\"\\nAgent created with custom SQL system prompt builder!\")\n    print(\"The agent will now use the SQL-focused system prompt for all interactions.\")\n\n\nif __name__ == \"__main__\":\n    import asyncio\n\n    asyncio.run(demo())\n"
  },
  {
    "path": "src/vanna/examples/default_workflow_handler_example.py",
    "content": "\"\"\"\nExample demonstrating the DefaultWorkflowHandler with setup health checking.\n\nThis example shows how the DefaultWorkflowHandler provides intelligent starter UI\nthat adapts based on available tools and helps users understand their setup status.\n\nRun:\n  PYTHONPATH=. python vanna/examples/default_workflow_handler_example.py\n\"\"\"\n\nimport asyncio\n\nfrom vanna import (\n    AgentConfig,\n    Agent,\n    MemoryConversationStore,\n    MockLlmService,\n    User,\n    DefaultWorkflowHandler,\n)\nfrom vanna.core.registry import ToolRegistry\nfrom vanna.core.user.resolver import SimpleUserResolver\nfrom vanna.tools import ListFilesTool\n\n\nasync def demonstrate_setup_scenarios():\n    \"\"\"Demonstrate different setup scenarios with DefaultWorkflowHandler.\"\"\"\n    print(\"🚀 Starting DefaultWorkflowHandler Setup Health Check Demo\\n\")\n\n    # Create basic components\n    llm_service = MockLlmService(response_content=\"I'm ready to help!\")\n    conversation_store = MemoryConversationStore()\n    user_resolver = SimpleUserResolver()\n\n    # Create test user\n    user = User(\n        id=\"user1\",\n        username=\"alice\",\n        email=\"alice@example.com\",\n        group_memberships=[\"user\"],\n    )\n\n    print(\"=\" * 60)\n    print(\"SCENARIO 1: Empty Setup (No Tools)\")\n    print(\"=\" * 60)\n\n    # Empty tool registry\n    empty_registry = ToolRegistry()\n\n    agent_empty = Agent(\n        llm_service=llm_service,\n        tool_registry=empty_registry,\n        user_resolver=user_resolver,\n        conversation_store=conversation_store,\n        config=AgentConfig(stream_responses=False),\n        workflow_handler=DefaultWorkflowHandler(),\n    )\n\n    print(\"📋 Starter UI for empty setup:\")\n    async for component in agent_empty.send_message(\n        request_context=user_resolver.create_request_context(\n            metadata={\"starter_ui_request\": True}\n        ),\n        message=\"\",\n        conversation_id=\"empty-setup\",\n    ):\n        if hasattr(component, \"simple_component\") and component.simple_component:\n            print(f\"  📄 {component.simple_component.text[:100]}...\")\n        elif hasattr(component, \"rich_component\"):\n            comp = component.rich_component\n            if hasattr(comp, \"title\"):\n                print(f\"  📊 {comp.title}: {comp.status} - {comp.description}\")\n            elif hasattr(comp, \"content\"):\n                print(f\"  📝 {comp.content[:100]}...\")\n\n    print(\"\\n\" + \"=\" * 60)\n    print(\"SCENARIO 2: Functional Setup (SQL + Basic Tools)\")\n    print(\"=\" * 60)\n\n    # Tool registry with SQL tool (simulated)\n    functional_registry = ToolRegistry()\n\n    # Register a mock SQL tool (we'll simulate by tool name)\n    list_tool = ListFilesTool()\n    list_tool.name = \"run_sql\"  # Simulate SQL tool\n    functional_registry.register(list_tool)\n\n    agent_functional = Agent(\n        llm_service=llm_service,\n        tool_registry=functional_registry,\n        user_resolver=user_resolver,\n        conversation_store=conversation_store,\n        config=AgentConfig(stream_responses=False),\n        workflow_handler=DefaultWorkflowHandler(),\n    )\n\n    print(\"📋 Starter UI for functional setup:\")\n    async for component in agent_functional.send_message(\n        request_context=user_resolver.create_request_context(\n            metadata={\"starter_ui_request\": True}\n        ),\n        message=\"\",\n        conversation_id=\"functional-setup\",\n    ):\n        if hasattr(component, \"simple_component\") and component.simple_component:\n            print(f\"  📄 {component.simple_component.text[:100]}...\")\n        elif hasattr(component, \"rich_component\"):\n            comp = component.rich_component\n            if hasattr(comp, \"title\"):\n                print(f\"  📊 {comp.title}: {comp.status} - {comp.description}\")\n            elif hasattr(comp, \"content\"):\n                print(f\"  📝 {comp.content[:100]}...\")\n\n    print(\"\\n\" + \"=\" * 60)\n    print(\"SCENARIO 3: Complete Setup (SQL + Memory + Visualization)\")\n    print(\"=\" * 60)\n\n    # Complete tool registry\n    complete_registry = ToolRegistry()\n\n    # Mock SQL tool\n    sql_tool = ListFilesTool()\n    sql_tool.name = \"run_sql\"\n    complete_registry.register(sql_tool)\n\n    # Mock memory tools\n    search_tool = ListFilesTool()\n    search_tool.name = \"search_saved_correct_tool_uses\"\n    complete_registry.register(search_tool)\n\n    save_tool = ListFilesTool()\n    save_tool.name = \"save_question_tool_args\"\n    complete_registry.register(save_tool)\n\n    # Mock visualization tool\n    viz_tool = ListFilesTool()\n    viz_tool.name = \"visualize_data\"\n    complete_registry.register(viz_tool)\n\n    agent_complete = Agent(\n        llm_service=llm_service,\n        tool_registry=complete_registry,\n        user_resolver=user_resolver,\n        conversation_store=conversation_store,\n        config=AgentConfig(stream_responses=False),\n        workflow_handler=DefaultWorkflowHandler(),\n    )\n\n    print(\"📋 Starter UI for complete setup:\")\n    async for component in agent_complete.send_message(\n        request_context=user_resolver.create_request_context(\n            metadata={\"starter_ui_request\": True}\n        ),\n        message=\"\",\n        conversation_id=\"complete-setup\",\n    ):\n        if hasattr(component, \"simple_component\") and component.simple_component:\n            print(f\"  📄 {component.simple_component.text[:100]}...\")\n        elif hasattr(component, \"rich_component\"):\n            comp = component.rich_component\n            if hasattr(comp, \"title\"):\n                print(f\"  📊 {comp.title}: {comp.status} - {comp.description}\")\n            elif hasattr(comp, \"content\"):\n                print(f\"  📝 {comp.content[:100]}...\")\n\n    print(\"\\n\" + \"=\" * 60)\n    print(\"SCENARIO 4: Testing Commands\")\n    print(\"=\" * 60)\n\n    print(\"📋 Testing /help command:\")\n    async for component in agent_complete.send_message(\n        request_context=user_resolver.create_request_context(),\n        message=\"/help\",\n        conversation_id=\"help-test\",\n    ):\n        if hasattr(component, \"rich_component\") and hasattr(\n            component.rich_component, \"content\"\n        ):\n            print(f\"  📝 Help: {component.rich_component.content[:200]}...\")\n\n    print(\"\\n📋 Testing /status command:\")\n    async for component in agent_complete.send_message(\n        request_context=user_resolver.create_request_context(),\n        message=\"/status\",\n        conversation_id=\"status-test\",\n    ):\n        if hasattr(component, \"rich_component\"):\n            comp = component.rich_component\n            if hasattr(comp, \"title\"):\n                print(f\"  📊 {comp.title}: {comp.status}\")\n            elif hasattr(comp, \"content\"):\n                print(f\"  📝 Status: {comp.content[:150]}...\")\n\n    print(\"\\n✅ Demo complete! The DefaultWorkflowHandler provides:\")\n    print(\"  • Smart setup health checking\")\n    print(\"  • Contextual starter UI based on available tools\")\n    print(\"  • Helpful error messages and setup guidance\")\n    print(\"  • Built-in command handling (/help, /status)\")\n    print(\"  • Automatic tool analysis and recommendations\")\n\n\nasync def main():\n    \"\"\"Run the DefaultWorkflowHandler demonstration.\"\"\"\n    await demonstrate_setup_scenarios()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "src/vanna/examples/email_auth_example.py",
    "content": "\"\"\"\nEmail authentication example for the Vanna Agents framework.\n\nThis example demonstrates how to create an agent with email-based authentication\nwhere users are prompted for their email address in chat and the system creates\na user profile based on that email.\n\n## What This Example Shows\n\n1. **UserService Implementation**: A demo `DemoEmailUserService` that:\n   - Stores users in memory\n   - Authenticates users by email validation\n   - Creates user profiles automatically\n   - Manages user permissions\n\n2. **Authentication Tool**: An `AuthTool` that:\n   - Takes an email address as input\n   - Uses the UserService to authenticate/create users\n   - Returns rich UI components for success/error feedback\n   - Provides structured results for the LLM\n\n3. **In-Chat Authentication Flow**: Shows how:\n   - Users can provide their email in natural conversation\n   - The agent can prompt for authentication when needed\n   - Authentication results are displayed with rich UI components\n   - The system maintains user context across conversations\n\n## Key Components\n\n- `DemoEmailUserService`: Implements the `UserService` interface\n- `AuthTool`: Implements the `Tool` interface for authentication\n- Rich UI components for authentication feedback\n- Integration with the agent's tool registry and conversation store\n\n## Usage\n\nInteractive: python -m vanna.examples.email_auth_example\n\n## Note\n\nThis example uses a simplified mock LLM that doesn't actually call tools.\nIn a real implementation with OpenAI or Anthropic, the LLM would automatically\ndetect email addresses in user messages and call the authenticate_user tool.\n\nFor production use, you would:\n- Replace DemoEmailUserService with a database-backed implementation\n- Add proper email validation and security measures\n- Implement session management in the server layer\n- Add proper error handling and rate limiting\n\"\"\"\n\nimport asyncio\nfrom typing import Any, Dict, Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom vanna import (\n    AgentConfig,\n    Agent,\n    MemoryConversationStore,\n    MockLlmService,\n    User,\n)\nfrom vanna.core import Tool, UserService\nfrom vanna.core import ToolContext, ToolResult\nfrom vanna.core.registry import ToolRegistry\nfrom vanna.core.components import UiComponent\nfrom vanna.core import RichComponent\n\n\n# Demo User Service Implementation\nclass DemoEmailUserService(UserService):\n    \"\"\"Demo user service that authenticates users by email.\"\"\"\n\n    def __init__(self):\n        \"\"\"Initialize with in-memory user store.\"\"\"\n        self._users: Dict[str, User] = {}  # user_id -> User\n        self._email_to_id: Dict[str, str] = {}  # email -> user_id\n\n    async def get_user(self, user_id: str) -> Optional[User]:\n        \"\"\"Get user by ID.\"\"\"\n        return self._users.get(user_id)\n\n    async def authenticate(self, credentials: Dict[str, Any]) -> Optional[User]:\n        \"\"\"Authenticate user by email.\"\"\"\n        email = credentials.get(\"email\")\n        if not email or not self._is_valid_email(email):\n            return None\n\n        # Check if user exists\n        user_id = self._email_to_id.get(email)\n        if user_id:\n            return self._users[user_id]\n\n        # Create new user\n        user_id = f\"user_{len(self._users) + 1}\"\n        username = email.split(\"@\")[0]\n\n        user = User(\n            id=user_id,\n            username=username,\n            email=email,\n            permissions=[\"basic_user\"],\n            metadata={\"auth_method\": \"email\"},\n        )\n\n        self._users[user_id] = user\n        self._email_to_id[email] = user_id\n        return user\n\n    async def has_permission(self, user: User, permission: str) -> bool:\n        \"\"\"Check if user has permission.\"\"\"\n        return permission in user.permissions\n\n    def _is_valid_email(self, email: str) -> bool:\n        \"\"\"Simple email validation.\"\"\"\n        return \"@\" in email and \".\" in email.split(\"@\")[1]\n\n\n# Authentication Tool\nclass AuthArgs(BaseModel):\n    \"\"\"Arguments for authentication.\"\"\"\n\n    email: str = Field(description=\"User's email address\")\n\n\nclass AuthTool(Tool[AuthArgs]):\n    \"\"\"Tool to authenticate users by email.\"\"\"\n\n    def __init__(self, user_service: DemoEmailUserService):\n        self.user_service = user_service\n\n    @property\n    def name(self) -> str:\n        return \"authenticate_user\"\n\n    @property\n    def description(self) -> str:\n        return \"Authenticate a user by their email address. Use this when the user provides an email.\"\n\n    def get_args_schema(self) -> Type[AuthArgs]:\n        return AuthArgs\n\n    async def execute(self, context: ToolContext, args: AuthArgs) -> ToolResult:\n        \"\"\"Execute authentication.\"\"\"\n        user = await self.user_service.authenticate({\"email\": args.email})\n\n        if user:\n            success_msg = (\n                f\"✅ Welcome {user.username}! You're now authenticated as {user.email}\"\n            )\n\n            auth_component = RichComponent(\n                type=\"status_card\",\n                data={\n                    \"title\": \"Authentication Success\",\n                    \"status\": \"success\",\n                    \"description\": success_msg,\n                    \"icon\": \"✅\",\n                    \"metadata\": {\n                        \"user_id\": user.id,\n                        \"username\": user.username,\n                        \"email\": user.email,\n                    },\n                },\n            )\n\n            return ToolResult(\n                success=True,\n                result_for_llm=f\"User successfully authenticated as {user.username} ({user.email}). They can now access personalized features.\",\n                ui_component=UiComponent(rich_component=auth_component),\n            )\n        else:\n            error_msg = f\"❌ Invalid email format: {args.email}\"\n            error_component = RichComponent(\n                type=\"status_card\",\n                data={\n                    \"title\": \"Authentication Failed\",\n                    \"status\": \"error\",\n                    \"description\": error_msg,\n                    \"icon\": \"❌\",\n                    \"metadata\": {\"email\": args.email},\n                },\n            )\n\n            return ToolResult(\n                success=False,\n                result_for_llm=f\"Authentication failed for {args.email}. Please provide a valid email address.\",\n                ui_component=UiComponent(rich_component=error_component),\n                error=error_msg,\n            )\n\n\ndef create_demo_agent() -> Agent:\n    \"\"\"Create a demo agent for REPL and server usage.\n\n    Returns:\n        Configured Agent instance with email authentication\n    \"\"\"\n    return create_auth_agent()\n\n\ndef create_auth_agent() -> Agent:\n    \"\"\"Create agent with email authentication.\"\"\"\n\n    # Create user service\n    user_service = DemoEmailUserService()\n\n    # Use simple mock LLM - the system prompt will guide behavior\n    llm_service = MockLlmService(\n        response_content=\"Hello! I'm your AI assistant. To provide you with personalized help, I'll need your email address for authentication. Please share your email with me, and I'll use the authenticate_user tool to set up your profile.\"\n    )\n\n    # Create tool registry with auth tool\n    tool_registry = ToolRegistry()\n    auth_tool = AuthTool(user_service)\n    tool_registry.register(auth_tool)\n\n    # Create agent with authentication system prompt\n    agent = Agent(\n        llm_service=llm_service,\n        config=AgentConfig(\n            stream_responses=True,\n            include_thinking_indicators=False,  # Cleaner output for demo\n            system_prompt=\"\"\"You are a helpful AI assistant with an email-based authentication system.\n\nAUTHENTICATION BEHAVIOR:\n1. When a user provides an email address in their message, immediately use the 'authenticate_user' tool\n2. Look for emails in patterns like \"my email is...\", \"I'm john@example.com\", or any text with @ symbols  \n3. If user isn't authenticated, politely ask for their email address to get started\n4. After successful authentication, welcome them by name and offer personalized assistance\n5. Be friendly and helpful throughout the process\n\nRemember: Authentication is required for personalized features!\"\"\",\n        ),\n        tool_registry=tool_registry,\n        conversation_store=MemoryConversationStore(),\n    )\n\n    return agent\n\n\nasync def demo_auth_flow():\n    \"\"\"Demonstrate the authentication flow with simple output.\"\"\"\n    agent = create_auth_agent()\n\n    # Start with anonymous user\n    user = User(id=\"anonymous\", username=\"guest\", email=None, permissions=[])\n    conversation_id = \"auth_demo_conv\"\n\n    print(\"=== Email Authentication Demo ===\")\n    print(\"This example shows how an agent can authenticate users via email in chat.\")\n    print(\"Note: This uses a simple mock LLM for demonstration purposes.\\n\")\n\n    # Demo conversation\n    print(\"🔹 Step 1: Initial greeting\")\n    print(\"User: Hello!\")\n    print(\"Agent: \", end=\"\")\n\n    async for component in agent.send_message(\n        user=user, message=\"Hello!\", conversation_id=conversation_id\n    ):\n        if (\n            hasattr(component, \"rich_component\")\n            and component.rich_component.type.value == \"text\"\n        ):\n            content = component.rich_component.data.get(\"content\") or getattr(\n                component.rich_component, \"content\", \"\"\n            )\n            if content:\n                print(content)\n                break\n\n    print(\"\\n\" + \"=\" * 60)\n\n    print(\"\\n🔹 Step 2: User provides email for authentication\")\n    print(\"User: My email is alice@example.com\")\n    print(\"Agent: \", end=\"\")\n\n    # This should trigger the auth tool\n    auth_shown = False\n    async for component in agent.send_message(\n        user=user,\n        message=\"My email is alice@example.com\",\n        conversation_id=conversation_id,\n    ):\n        if hasattr(component, \"rich_component\"):\n            rich_comp = component.rich_component\n            if rich_comp.type.value == \"status_card\" and not auth_shown:\n                status = rich_comp.data.get(\"status\", \"\")\n                desc = rich_comp.data.get(\"description\", \"\")\n                if status == \"success\":\n                    auth_shown = True\n                    print(f\"🔐 {desc}\")\n                    break\n\n    print(\"\\n\" + \"=\" * 60)\n\n    print(\"\\n🔹 Step 3: Post-authentication interaction\")\n    print(\"User: What can you help me with now?\")\n    print(\"Agent: \", end=\"\")\n\n    async for component in agent.send_message(\n        user=user,\n        message=\"What can you help me with now?\",\n        conversation_id=conversation_id,\n    ):\n        if (\n            hasattr(component, \"rich_component\")\n            and component.rich_component.type.value == \"text\"\n        ):\n            content = component.rich_component.data.get(\"content\") or getattr(\n                component.rich_component, \"content\", \"\"\n            )\n            if content:\n                print(content)\n                break\n\n    print(\"\\n\" + \"=\" * 60)\n    print(\"\\n✅ Authentication demo complete!\")\n    print(\"\\nKey Features Demonstrated:\")\n    print(\"• Email-based user authentication\")\n    print(\"• Tool-based authentication flow\")\n    print(\"• In-memory user storage and management\")\n    print(\"• Rich UI components for auth feedback\")\n\n\nasync def main():\n    \"\"\"Run the authentication example.\"\"\"\n    await demo_auth_flow()\n\n\ndef run_interactive():\n    \"\"\"Entry point for interactive usage.\"\"\"\n    print(\"Starting email authentication example...\")\n    asyncio.run(main())\n\n\nif __name__ == \"__main__\":\n    run_interactive()\n"
  },
  {
    "path": "src/vanna/examples/evaluation_example.py",
    "content": "\"\"\"\nEvaluation System Example\n\nThis example demonstrates how to use the evaluation framework to test\nand compare agents. Shows:\n- Creating test cases programmatically\n- Running evaluations with multiple evaluators\n- Comparing agent variants (e.g., different LLMs)\n- Generating reports\n\nUsage:\n    PYTHONPATH=. python vanna/examples/evaluation_example.py\n\"\"\"\n\nimport asyncio\nfrom vanna import Agent, MockLlmService, MemoryConversationStore, User\nfrom vanna.core.evaluation import (\n    EvaluationRunner,\n    EvaluationDataset,\n    TestCase,\n    ExpectedOutcome,\n    AgentVariant,\n    TrajectoryEvaluator,\n    OutputEvaluator,\n    EfficiencyEvaluator,\n)\nfrom vanna.core.registry import ToolRegistry\n\n\ndef create_sample_dataset() -> EvaluationDataset:\n    \"\"\"Create a sample dataset for demonstration.\"\"\"\n\n    eval_user = User(\n        id=\"eval_user\", username=\"evaluator\", email=\"eval@example.com\", permissions=[]\n    )\n\n    test_cases = [\n        TestCase(\n            id=\"test_001\",\n            user=eval_user,\n            message=\"Hello, how are you?\",\n            expected_outcome=ExpectedOutcome(\n                final_answer_contains=[\"hello\", \"hi\"],\n                max_execution_time_ms=3000,\n            ),\n            metadata={\"category\": \"greeting\", \"difficulty\": \"easy\"},\n        ),\n        TestCase(\n            id=\"test_002\",\n            user=eval_user,\n            message=\"What can you help me with?\",\n            expected_outcome=ExpectedOutcome(\n                final_answer_contains=[\"help\", \"assist\"],\n                max_execution_time_ms=3000,\n            ),\n            metadata={\"category\": \"capabilities\", \"difficulty\": \"easy\"},\n        ),\n        TestCase(\n            id=\"test_003\",\n            user=eval_user,\n            message=\"Explain quantum computing\",\n            expected_outcome=ExpectedOutcome(\n                final_answer_contains=[\"quantum\", \"computing\"],\n                min_components=1,\n                max_execution_time_ms=5000,\n            ),\n            metadata={\"category\": \"explanation\", \"difficulty\": \"medium\"},\n        ),\n    ]\n\n    return EvaluationDataset(\n        name=\"Demo Test Cases\",\n        test_cases=test_cases,\n        description=\"Sample test cases for evaluation demo\",\n    )\n\n\ndef create_test_agent(name: str, response_content: str) -> Agent:\n    \"\"\"Create a test agent with mock LLM.\"\"\"\n    return Agent(\n        llm_service=MockLlmService(response_content=response_content),\n        tool_registry=ToolRegistry(),\n        conversation_store=MemoryConversationStore(),\n    )\n\n\nasync def demo_single_agent_evaluation():\n    \"\"\"Demonstrate evaluating a single agent.\"\"\"\n    print(\"\\n\" + \"=\" * 80)\n    print(\"DEMO 1: Single Agent Evaluation\")\n    print(\"=\" * 80 + \"\\n\")\n\n    # Create dataset\n    dataset = create_sample_dataset()\n    print(f\"Loaded dataset: {dataset.name}\")\n    print(f\"Test cases: {len(dataset.test_cases)}\\n\")\n\n    # Create agent\n    agent = create_test_agent(\n        \"test-agent\",\n        \"Hello! I'm here to help you with various tasks including answering questions about topics like quantum computing.\",\n    )\n\n    # Create evaluators\n    evaluators = [\n        TrajectoryEvaluator(),\n        OutputEvaluator(),\n        EfficiencyEvaluator(max_execution_time_ms=5000),\n    ]\n\n    # Run evaluation\n    runner = EvaluationRunner(evaluators=evaluators, max_concurrency=5)\n    print(\"Running evaluation...\")\n    report = await runner.run_evaluation(agent, dataset.test_cases)\n\n    # Print results\n    report.print_summary()\n\n    # Show failures\n    failures = report.get_failures()\n    if failures:\n        print(\"\\nFailed test cases:\")\n        for result in failures:\n            print(f\"  - {result.test_case.id}: {result.test_case.message}\")\n\n\nasync def demo_agent_comparison():\n    \"\"\"Demonstrate comparing multiple agent variants.\"\"\"\n    print(\"\\n\" + \"=\" * 80)\n    print(\"DEMO 2: Agent Comparison (LLM Comparison Use Case)\")\n    print(\"=\" * 80 + \"\\n\")\n\n    # Create dataset\n    dataset = create_sample_dataset()\n    print(f\"Loaded dataset: {dataset.name}\")\n    print(f\"Test cases: {len(dataset.test_cases)}\\n\")\n\n    # Create agent variants\n    variants = [\n        AgentVariant(\n            name=\"agent-v1\",\n            agent=create_test_agent(\n                \"v1\",\n                \"Hi there! I can help you with many things including explaining complex topics like quantum computing.\",\n            ),\n            metadata={\"version\": \"1.0\", \"model\": \"mock-v1\"},\n        ),\n        AgentVariant(\n            name=\"agent-v2\",\n            agent=create_test_agent(\n                \"v2\",\n                \"Hello! I'm your helpful assistant. I can assist with various tasks and explain topics like quantum computing in detail.\",\n            ),\n            metadata={\"version\": \"2.0\", \"model\": \"mock-v2\"},\n        ),\n        AgentVariant(\n            name=\"agent-v3\",\n            agent=create_test_agent(\n                \"v3\",\n                \"Greetings! I'm designed to help you with a wide range of tasks, from simple questions to complex explanations about quantum computing and more.\",\n            ),\n            metadata={\"version\": \"3.0\", \"model\": \"mock-v3\"},\n        ),\n    ]\n\n    print(f\"Created {len(variants)} agent variants:\")\n    for v in variants:\n        print(f\"  - {v.name}\")\n    print()\n\n    # Create evaluators\n    evaluators = [\n        OutputEvaluator(),\n        EfficiencyEvaluator(max_execution_time_ms=5000),\n    ]\n\n    # Run comparison\n    runner = EvaluationRunner(evaluators=evaluators, max_concurrency=10)\n    print(\n        f\"Running comparison ({len(variants)} variants × {len(dataset.test_cases)} test cases)...\"\n    )\n    print(\"All variants running in parallel for maximum efficiency...\\n\")\n\n    comparison = await runner.compare_agents(variants, dataset.test_cases)\n\n    # Print results\n    comparison.print_summary()\n\n    # Show best variants\n    print(\"Best Performing Variants:\")\n    print(f\"  🏆 Best score: {comparison.get_best_variant('score')}\")\n    print(f\"  ⚡ Fastest: {comparison.get_best_variant('speed')}\")\n    print(f\"  ✅ Best pass rate: {comparison.get_best_variant('pass_rate')}\")\n\n\nasync def demo_dataset_operations():\n    \"\"\"Demonstrate dataset creation and manipulation.\"\"\"\n    print(\"\\n\" + \"=\" * 80)\n    print(\"DEMO 3: Dataset Operations\")\n    print(\"=\" * 80 + \"\\n\")\n\n    # Create dataset\n    dataset = create_sample_dataset()\n\n    # Show dataset info\n    print(f\"Dataset: {dataset.name}\")\n    print(f\"Description: {dataset.description}\")\n    print(f\"Total test cases: {len(dataset)}\\n\")\n\n    # Filter by metadata\n    easy_tests = dataset.filter_by_metadata(difficulty=\"easy\")\n    medium_tests = dataset.filter_by_metadata(difficulty=\"medium\")\n\n    print(f\"Easy test cases: {len(easy_tests)}\")\n    print(f\"Medium test cases: {len(medium_tests)}\\n\")\n\n    # Save to file (for demonstration)\n    import tempfile\n    import os\n\n    with tempfile.TemporaryDirectory() as tmpdir:\n        yaml_path = os.path.join(tmpdir, \"dataset.yaml\")\n        json_path = os.path.join(tmpdir, \"dataset.json\")\n\n        dataset.save_yaml(yaml_path)\n        dataset.save_json(json_path)\n\n        print(\"Dataset saved to temporary files:\")\n        print(f\"  - YAML: {yaml_path}\")\n        print(f\"  - JSON: {json_path}\\n\")\n\n        # Load back\n        loaded_yaml = EvaluationDataset.from_yaml(yaml_path)\n        loaded_json = EvaluationDataset.from_json(json_path)\n\n        print(\"Loaded datasets:\")\n        print(f\"  - From YAML: {len(loaded_yaml)} test cases\")\n        print(f\"  - From JSON: {len(loaded_json)} test cases\")\n\n\nasync def main():\n    \"\"\"Run all evaluation demos.\"\"\"\n    print(\"\\n🚀 Vanna Agents Evaluation System Demo\")\n    print(\"=\" * 80)\n\n    # Demo 1: Single agent evaluation\n    await demo_single_agent_evaluation()\n\n    # Demo 2: Agent comparison (main use case)\n    await demo_agent_comparison()\n\n    # Demo 3: Dataset operations\n    await demo_dataset_operations()\n\n    print(\"\\n\" + \"=\" * 80)\n    print(\"✅ All demos completed!\")\n    print(\"=\" * 80)\n    print(\"\\nKey Takeaways:\")\n    print(\"  1. Evaluations are integral to the Vanna package\")\n    print(\"  2. Parallel execution handles I/O-bound LLM calls efficiently\")\n    print(\"  3. Agent comparison is a first-class use case\")\n    print(\"  4. Multiple evaluators can be composed for comprehensive testing\")\n    print(\"  5. Reports can be exported to HTML, CSV, or printed to console\")\n    print(\"\\nFor LLM comparison, see: evals/benchmarks/llm_comparison.py\")\n    print()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "src/vanna/examples/extensibility_example.py",
    "content": "\"\"\"\nComprehensive example demonstrating all extensibility interfaces.\n\nThis example shows how to use:\n- LlmMiddleware for caching\n- ErrorRecoveryStrategy for retry logic\n- ToolContextEnricher for adding user preferences\n- ConversationFilter for context window management\n- ObservabilityProvider for monitoring\n\"\"\"\n\nimport asyncio\nimport time\nfrom typing import Any, Dict, List, Optional\n\nfrom vanna.core import (\n    Agent,\n    LlmMiddleware,\n    ErrorRecoveryStrategy,\n    ToolContextEnricher,\n    ConversationFilter,\n    ObservabilityProvider,\n    User,\n    ToolContext,\n    Conversation,\n    Message,\n    LlmRequest,\n    LlmResponse,\n    Span,\n    Metric,\n)\nfrom vanna.core.recovery import RecoveryAction, RecoveryActionType\nfrom vanna.core.registry import ToolRegistry\n\n\n# 1. LlmMiddleware Example: Simple Caching\nclass CachingMiddleware(LlmMiddleware):\n    \"\"\"Cache LLM responses to reduce costs and latency.\"\"\"\n\n    def __init__(self) -> None:\n        self.cache: Dict[str, LlmResponse] = {}\n        self.hits = 0\n        self.misses = 0\n\n    def _compute_cache_key(self, request: LlmRequest) -> str:\n        \"\"\"Create cache key from request.\"\"\"\n        messages_str = str([(m.role, m.content) for m in request.messages])\n        return f\"{messages_str}:{request.temperature}\"\n\n    async def before_llm_request(self, request: LlmRequest) -> LlmRequest:\n        \"\"\"Check cache before sending request.\"\"\"\n        cache_key = self._compute_cache_key(request)\n        if cache_key in self.cache:\n            self.hits += 1\n            print(f\"[CACHE HIT] Cache stats: {self.hits} hits, {self.misses} misses\")\n        return request\n\n    async def after_llm_response(\n        self, request: LlmRequest, response: LlmResponse\n    ) -> LlmResponse:\n        \"\"\"Cache the response.\"\"\"\n        cache_key = self._compute_cache_key(request)\n        if cache_key not in self.cache:\n            self.cache[cache_key] = response\n            self.misses += 1\n            print(f\"[CACHE MISS] Caching response\")\n        return response\n\n\n# 2. ErrorRecoveryStrategy Example: Exponential Backoff\nclass ExponentialBackoffStrategy(ErrorRecoveryStrategy):\n    \"\"\"Retry failed operations with exponential backoff.\"\"\"\n\n    def __init__(self, max_retries: int = 3) -> None:\n        self.max_retries = max_retries\n\n    async def handle_tool_error(\n        self, error: Exception, context: ToolContext, attempt: int = 1\n    ) -> RecoveryAction:\n        \"\"\"Retry tool errors with exponential backoff.\"\"\"\n        if attempt < self.max_retries:\n            delay_ms = (2 ** (attempt - 1)) * 1000\n            print(\n                f\"[RETRY] Tool failed, retrying in {delay_ms}ms (attempt {attempt}/{self.max_retries})\"\n            )\n            return RecoveryAction(\n                action=RecoveryActionType.RETRY,\n                retry_delay_ms=delay_ms,\n                message=f\"Retrying after {delay_ms}ms\",\n            )\n\n        print(f\"[FAIL] Max retries exceeded for tool error: {error}\")\n        return RecoveryAction(\n            action=RecoveryActionType.FAIL,\n            message=f\"Tool error after {self.max_retries} attempts: {str(error)}\",\n        )\n\n    async def handle_llm_error(\n        self, error: Exception, request: LlmRequest, attempt: int = 1\n    ) -> RecoveryAction:\n        \"\"\"Retry LLM errors with backoff.\"\"\"\n        if attempt < self.max_retries:\n            delay_ms = (2 ** (attempt - 1)) * 1000\n            print(\n                f\"[RETRY] LLM failed, retrying in {delay_ms}ms (attempt {attempt}/{self.max_retries})\"\n            )\n            return RecoveryAction(\n                action=RecoveryActionType.RETRY,\n                retry_delay_ms=delay_ms,\n                message=f\"Retrying LLM after {delay_ms}ms\",\n            )\n\n        print(f\"[FAIL] Max retries exceeded for LLM error: {error}\")\n        return RecoveryAction(\n            action=RecoveryActionType.FAIL,\n            message=f\"LLM error after {self.max_retries} attempts: {str(error)}\",\n        )\n\n\n# 3. ToolContextEnricher Example: Add User Preferences\nclass UserPreferencesEnricher(ToolContextEnricher):\n    \"\"\"Enrich context with user preferences.\"\"\"\n\n    def __init__(self) -> None:\n        # Mock user preferences database\n        self.preferences: Dict[str, Dict[str, Any]] = {\n            \"user123\": {\n                \"timezone\": \"America/New_York\",\n                \"language\": \"en\",\n                \"theme\": \"dark\",\n            }\n        }\n\n    async def enrich_context(self, context: ToolContext) -> ToolContext:\n        \"\"\"Add user preferences to context.\"\"\"\n        prefs = self.preferences.get(context.user.id, {})\n        context.metadata[\"user_preferences\"] = prefs\n        context.metadata[\"timezone\"] = prefs.get(\"timezone\", \"UTC\")\n        print(f\"[ENRICH] Added preferences for user {context.user.id}: {prefs}\")\n        return context\n\n\n# 4. ConversationFilter Example: Context Window Management\nclass ContextWindowFilter(ConversationFilter):\n    \"\"\"Limit conversation to fit within context window.\"\"\"\n\n    def __init__(self, max_messages: int = 20) -> None:\n        self.max_messages = max_messages\n\n    async def filter_messages(self, messages: List[Message]) -> List[Message]:\n        \"\"\"Keep only recent messages within limit.\"\"\"\n        if len(messages) <= self.max_messages:\n            return messages\n\n        # Keep system messages and recent messages\n        system_messages = [m for m in messages if m.role == \"system\"]\n        other_messages = [m for m in messages if m.role != \"system\"]\n\n        # Take the most recent messages\n        recent_messages = other_messages[-self.max_messages :]\n        filtered = system_messages + recent_messages\n\n        print(f\"[FILTER] Reduced {len(messages)} messages to {len(filtered)}\")\n        return filtered\n\n\n# 5. ObservabilityProvider Example: Simple Logging\nclass LoggingObservabilityProvider(ObservabilityProvider):\n    \"\"\"Log metrics and spans for monitoring.\"\"\"\n\n    def __init__(self) -> None:\n        self.metrics: List[Metric] = []\n        self.spans: List[Span] = []\n\n    async def record_metric(\n        self,\n        name: str,\n        value: float,\n        unit: str = \"\",\n        tags: Optional[Dict[str, str]] = None,\n    ) -> None:\n        \"\"\"Record and log a metric.\"\"\"\n        metric = Metric(name=name, value=value, unit=unit, tags=tags or {})\n        self.metrics.append(metric)\n        tags_str = \", \".join(f\"{k}={v}\" for k, v in (tags or {}).items())\n        print(f\"[METRIC] {name}: {value}{unit} {tags_str}\")\n\n    async def create_span(\n        self, name: str, attributes: Optional[Dict[str, Any]] = None\n    ) -> Span:\n        \"\"\"Create a span for tracing.\"\"\"\n        span = Span(name=name, attributes=attributes or {})\n        print(f\"[SPAN START] {name}\")\n        return span\n\n    async def end_span(self, span: Span) -> None:\n        \"\"\"End and record a span.\"\"\"\n        span.end()\n        self.spans.append(span)\n        duration = span.duration_ms() or 0\n        print(f\"[SPAN END] {span.name}: {duration:.2f}ms\")\n\n\nasync def run_example() -> None:\n    \"\"\"\n    Example showing all extensibility interfaces working together.\n    \"\"\"\n    from vanna.integrations.anthropic import AnthropicLlmService\n\n    # Create all extensibility components\n    caching_middleware = CachingMiddleware()\n    retry_strategy = ExponentialBackoffStrategy(max_retries=3)\n    preferences_enricher = UserPreferencesEnricher()\n    context_filter = ContextWindowFilter(max_messages=20)\n    observability = LoggingObservabilityProvider()\n\n    # Mock conversation store\n    class MockStore:\n        async def get_conversation(self, cid: str, uid: str) -> Optional[Conversation]:\n            return None\n\n        async def create_conversation(\n            self, cid: str, uid: str, title: str\n        ) -> Conversation:\n            return Conversation(\n                id=cid, user_id=uid, messages=[Message(role=\"user\", content=title)]\n            )\n\n        async def update_conversation(self, conv: Conversation) -> None:\n            pass\n\n        async def delete_conversation(self, cid: str, uid: str) -> bool:\n            return False\n\n        async def list_conversations(\n            self, uid: str, limit: int = 50, offset: int = 0\n        ) -> List[Conversation]:\n            return []\n\n    # Create agent with all extensibility components\n    agent = Agent(\n        llm_service=AnthropicLlmService(api_key=\"test-key\"),\n        tool_registry=ToolRegistry(),\n        conversation_store=MockStore(),  # type: ignore\n        llm_middlewares=[caching_middleware],\n        error_recovery_strategy=retry_strategy,\n        context_enrichers=[preferences_enricher],\n        conversation_filters=[context_filter],\n        observability_provider=observability,\n    )\n\n    print(\"✓ Agent created with all extensibility components:\")\n    print(f\"  - LLM Middleware: {len(agent.llm_middlewares)} middlewares\")\n    print(f\"  - Error Recovery: {type(agent.error_recovery_strategy).__name__}\")\n    print(f\"  - Context Enrichers: {len(agent.context_enrichers)} enrichers\")\n    print(f\"  - Conversation Filters: {len(agent.conversation_filters)} filters\")\n    print(f\"  - Observability: {type(agent.observability_provider).__name__}\")\n    print(\"\\n🎉 All extensibility interfaces integrated successfully!\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(run_example())\n"
  },
  {
    "path": "src/vanna/examples/minimal_example.py",
    "content": "\"\"\"Minimal Claude + SQLite example ready for FastAPI.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom pathlib import Path\n\nfrom vanna import AgentConfig, Agent\nfrom vanna.core.registry import ToolRegistry\nfrom vanna.integrations.anthropic import AnthropicLlmService\nfrom vanna.integrations.sqlite import SqliteRunner\nfrom vanna.integrations.local import LocalFileSystem\nfrom vanna.tools import (\n    RunSqlTool,\n    # Visualization\n    VisualizeDataTool,\n    # Python execution\n    RunPythonFileTool,\n    PipInstallTool,\n    # File system (for coding agents)\n    SearchFilesTool,\n    ListFilesTool,\n    ReadFileTool,\n    WriteFileTool,\n)\n\n_DB = Path(__file__).resolve().parents[2] / \"Chinook.sqlite\"\n\n\ndef create_demo_agent() -> Agent:\n    # Load environment variables from .env file\n    from dotenv import load_dotenv\n\n    load_dotenv()\n\n    llm = AnthropicLlmService(model=os.getenv(\"ANTHROPIC_MODEL\", \"claude-sonnet-4-5\"))\n\n    # Shared file system for all tools\n    file_system = LocalFileSystem(\"./claude_data\")\n\n    tools = ToolRegistry()\n\n    # 1. Basic SQL agent - query databases\n    tools.register(\n        RunSqlTool(\n            sql_runner=SqliteRunner(database_path=str(_DB)),\n            file_system=file_system,\n        )\n    )\n\n    # 2. Add visualization - create charts from data\n    tools.register(VisualizeDataTool(file_system=file_system))\n\n    # 3. Add Python execution - build dashboards with artifacts\n    # tools.register(RunPythonFileTool(file_system=file_system))\n    # tools.register(PipInstallTool(file_system=file_system))\n\n    # 4. Full coding agent - read, write, search files\n    # tools.register(SearchFilesTool(file_system=file_system))\n    # tools.register(ListFilesTool(file_system=file_system))\n    # tools.register(ReadFileTool(file_system=file_system))\n    # tools.register(WriteFileTool(file_system=file_system))\n\n    return Agent(\n        llm_service=llm,\n        tool_registry=tools,\n    )\n"
  },
  {
    "path": "src/vanna/examples/mock_auth_example.py",
    "content": "\"\"\"\nMock authentication example to verify user resolution is working.\n\nThis example demonstrates the new UserResolver architecture where:\n1. UserResolver is a required parameter of Agent\n2. Agent.send_message() accepts RequestContext (not User directly)\n3. The Agent resolves the user internally using the UserResolver\n\nThe agent uses an LLM middleware to inject user info into the response,\nso we can verify the authentication is working correctly.\n\nUsage:\n    python -m vanna.examples.mock_auth_example\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\n\nfrom vanna import AgentConfig, Agent\nfrom vanna.core.registry import ToolRegistry\nfrom vanna.core.llm import LlmRequest, LlmResponse\nfrom vanna.core.middleware import LlmMiddleware\nfrom vanna.integrations.mock import MockLlmService\nfrom vanna.core.user import CookieEmailUserResolver, RequestContext\n\n\nclass UserEchoMiddleware(LlmMiddleware):\n    \"\"\"Middleware that injects user email into LLM responses.\"\"\"\n\n    async def after_llm_response(\n        self, request: LlmRequest, response: LlmResponse\n    ) -> LlmResponse:\n        \"\"\"Inject user email into response.\"\"\"\n        # Extract user email from request user_id (which is set to user.id in the agent)\n        user_id = request.user_id\n\n        # Create a new response with user info\n        new_content = f\"Hello! You are authenticated as: {user_id}\"\n\n        return LlmResponse(\n            content=new_content,\n            finish_reason=response.finish_reason,\n            usage=response.usage,\n        )\n\n\ndef create_demo_agent() -> Agent:\n    \"\"\"Create a demo agent for server usage.\n\n    Returns:\n        Configured Agent instance with cookie-based authentication\n    \"\"\"\n    # Create a mock LLM\n    llm_service = MockLlmService(response_content=\"Mock response\")\n\n    # Empty tool registry\n    tool_registry = ToolRegistry()\n\n    # Cookie-based user resolver\n    user_resolver = CookieEmailUserResolver(cookie_name=\"vanna_email\")\n\n    # User echo middleware\n    middleware = UserEchoMiddleware()\n\n    # Create agent with user resolver and middleware\n    agent = Agent(\n        llm_service=llm_service,\n        tool_registry=tool_registry,\n        user_resolver=user_resolver,\n        llm_middlewares=[middleware],\n        config=AgentConfig(\n            stream_responses=True,\n            include_thinking_indicators=False,\n        ),\n    )\n\n    return agent\n\n\nasync def demo_authentication():\n    \"\"\"Demonstrate authentication with different request contexts.\"\"\"\n    agent = create_demo_agent()\n\n    print(\"=== Mock Authentication Demo ===\")\n    print(\"This example verifies that user resolution is working correctly.\\n\")\n\n    # Test 1: Request with email cookie\n    print(\"🔹 Test 1: Authenticated user (alice@example.com)\")\n    request_context = RequestContext(\n        cookies={\"vanna_email\": \"alice@example.com\"},\n        headers={},\n        remote_addr=\"127.0.0.1\",\n    )\n\n    print(\n        \"Request context:\",\n        {\n            \"cookies\": request_context.cookies,\n            \"headers\": request_context.headers,\n            \"remote_addr\": request_context.remote_addr,\n        },\n    )\n\n    # Send message - Agent will resolve user internally\n    agent_response = \"\"\n    async for component in agent.send_message(\n        request_context=request_context,\n        message=\"Who am I?\",\n        conversation_id=\"test_conv_1\",\n    ):\n        # Extract and display user info from the resolved user\n        if hasattr(component, \"rich_component\"):\n            rich = component.rich_component\n            # Check if it's a text component\n            if rich.type.value == \"text\":\n                # Access content directly from the component (before serialization)\n                if hasattr(rich, \"content\"):\n                    agent_response = rich.content\n\n    print(f\"Agent response: {agent_response}\")\n\n    # Verify user was resolved by checking the conversation store\n    user_resolver = agent.user_resolver\n    resolved_user = await user_resolver.resolve_user(request_context)\n    print(\n        f\"✅ Resolved user: {resolved_user.email} (username: {resolved_user.username}, id: {resolved_user.id})\"\n    )\n    print(f\"   Permissions: {resolved_user.permissions}\")\n    print(f\"   Metadata: {resolved_user.metadata}\")\n\n    print(\"\\n\" + \"=\" * 60 + \"\\n\")\n\n    # Test 2: Request without email cookie (anonymous)\n    print(\"🔹 Test 2: Anonymous user (no cookie)\")\n    anonymous_context = RequestContext(cookies={}, headers={}, remote_addr=\"127.0.0.1\")\n\n    print(\n        \"Request context:\",\n        {\n            \"cookies\": anonymous_context.cookies,\n            \"headers\": anonymous_context.headers,\n            \"remote_addr\": anonymous_context.remote_addr,\n        },\n    )\n\n    agent_response = \"\"\n    async for component in agent.send_message(\n        request_context=anonymous_context,\n        message=\"Who am I?\",\n        conversation_id=\"test_conv_2\",\n    ):\n        if hasattr(component, \"rich_component\"):\n            rich = component.rich_component\n            if rich.type.value == \"text\" and hasattr(rich, \"content\"):\n                agent_response = rich.content\n\n    print(f\"Agent response: {agent_response}\")\n\n    resolved_user = await user_resolver.resolve_user(anonymous_context)\n    print(\n        f\"✅ Resolved user: {resolved_user.email or 'None'} (username: {resolved_user.username}, id: {resolved_user.id})\"\n    )\n    print(f\"   Permissions: {resolved_user.permissions}\")\n    print(f\"   Metadata: {resolved_user.metadata}\")\n\n    print(\"\\n\" + \"=\" * 60 + \"\\n\")\n\n    # Test 3: Different user\n    print(\"🔹 Test 3: Different authenticated user (bob@company.com)\")\n    bob_context = RequestContext(\n        cookies={\"vanna_email\": \"bob@company.com\"},\n        headers={\"User-Agent\": \"Mozilla/5.0\"},\n        remote_addr=\"192.168.1.100\",\n    )\n\n    print(\n        \"Request context:\",\n        {\n            \"cookies\": bob_context.cookies,\n            \"headers\": bob_context.headers,\n            \"remote_addr\": bob_context.remote_addr,\n        },\n    )\n\n    agent_response = \"\"\n    async for component in agent.send_message(\n        request_context=bob_context, message=\"Who am I?\", conversation_id=\"test_conv_3\"\n    ):\n        if hasattr(component, \"rich_component\"):\n            rich = component.rich_component\n            if rich.type.value == \"text\" and hasattr(rich, \"content\"):\n                agent_response = rich.content\n\n    print(f\"Agent response: {agent_response}\")\n\n    resolved_user = await user_resolver.resolve_user(bob_context)\n    print(\n        f\"✅ Resolved user: {resolved_user.email} (username: {resolved_user.username}, id: {resolved_user.id})\"\n    )\n    print(f\"   Permissions: {resolved_user.permissions}\")\n    print(f\"   Metadata: {resolved_user.metadata}\")\n\n    print(\"\\n\" + \"=\" * 60)\n    print(\"\\n✅ Authentication demo complete!\")\n    print(\"\\nKey Features Verified:\")\n    print(\"• UserResolver is part of Agent\")\n    print(\"• Agent.send_message() accepts RequestContext\")\n    print(\"• User resolution happens internally in Agent\")\n    print(\"• CookieEmailUserResolver extracts email from vanna_email cookie\")\n    print(\"• Anonymous users are created when no cookie is present\")\n    print(\"• Different users can be resolved from different request contexts\")\n\n\nasync def main():\n    \"\"\"Run the authentication example.\"\"\"\n    await demo_authentication()\n\n\ndef run_interactive():\n    \"\"\"Entry point for interactive usage.\"\"\"\n    print(\"Starting mock authentication example...\")\n    asyncio.run(main())\n\n\nif __name__ == \"__main__\":\n    run_interactive()\n"
  },
  {
    "path": "src/vanna/examples/mock_custom_tool.py",
    "content": "\"\"\"\nMock example showing how to create and use custom tools.\n\nThis example demonstrates creating a simple calculator tool\nand registering it with an agent that uses a mock LLM service.\nIt now includes a `MockCalculatorLlmService` that automatically\ninvokes the calculator tool with random numbers before echoing\nback the computed answer.\n\nUsage:\n  Template: Copy this file and modify for your custom tools\n  Interactive: python -m vanna.examples.mock_custom_tool\n  REPL: from vanna.examples.mock_custom_tool import create_demo_agent\n  Server: python -m vanna.servers --example mock_custom_tool\n\"\"\"\n\nimport asyncio\nimport random\nimport uuid\nfrom typing import AsyncGenerator, Dict, List, Optional, Tuple, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom vanna import (\n    AgentConfig,\n    Agent,\n    Tool,\n    ToolContext,\n    ToolRegistry,\n    ToolResult,\n    User,\n    UiComponent,\n)\nfrom vanna.core.interfaces import LlmService\nfrom vanna.core.models import (\n    LlmRequest,\n    LlmResponse,\n    LlmStreamChunk,\n    ToolCall,\n    ToolSchema,\n)\nfrom vanna.core.rich_components import (\n    CardComponent,\n    NotificationComponent,\n    ComponentType,\n)\nfrom vanna.core.simple_components import (\n    SimpleTextComponent,\n)\n\n\nclass CalculatorArgs(BaseModel):\n    \"\"\"Arguments for the calculator tool.\"\"\"\n\n    operation: str = Field(\n        description=\"The operation to perform: add, subtract, multiply, divide\"\n    )\n    a: float = Field(description=\"First number\")\n    b: float = Field(description=\"Second number\")\n\n\nclass CalculatorTool(Tool[CalculatorArgs]):\n    \"\"\"A simple calculator tool.\"\"\"\n\n    @property\n    def name(self) -> str:\n        return \"calculator\"\n\n    @property\n    def description(self) -> str:\n        return \"Perform basic arithmetic operations (add, subtract, multiply, divide)\"\n\n    def get_args_schema(self) -> Type[CalculatorArgs]:\n        return CalculatorArgs\n\n    async def execute(self, context: ToolContext, args: CalculatorArgs) -> ToolResult:\n        \"\"\"Execute the calculator operation.\"\"\"\n        symbol_map = {\"add\": \"+\", \"subtract\": \"-\", \"multiply\": \"×\", \"divide\": \"÷\"}\n\n        try:\n            if args.operation == \"add\":\n                result = args.a + args.b\n            elif args.operation == \"subtract\":\n                result = args.a - args.b\n            elif args.operation == \"multiply\":\n                result = args.a * args.b\n            elif args.operation == \"divide\":\n                if args.b == 0:\n                    message = \"Cannot divide by zero\"\n                    await asyncio.sleep(3)\n                    return ToolResult(\n                        success=False,\n                        result_for_llm=message,\n                        ui_component=UiComponent(\n                            rich_component=NotificationComponent(\n                                type=ComponentType.NOTIFICATION,\n                                level=\"error\",\n                                message=message,\n                            ),\n                            simple_component=SimpleTextComponent(text=message),\n                        ),\n                        error=message,\n                    )\n                result = args.a / args.b\n            else:\n                message = f\"Unknown operation: {args.operation}\"\n                await asyncio.sleep(3)\n                return ToolResult(\n                    success=False,\n                    result_for_llm=message,\n                    ui_component=UiComponent(\n                        rich_component=NotificationComponent(\n                            type=ComponentType.NOTIFICATION,\n                            level=\"warning\",\n                            message=message,\n                        ),\n                        simple_component=SimpleTextComponent(text=message),\n                    ),\n                    error=message,\n                )\n\n            await asyncio.sleep(3)\n\n            symbol = symbol_map.get(args.operation, args.operation)\n            expression = f\"{args.a:g} {symbol} {args.b:g} = {result:g}\"\n            return ToolResult(\n                success=True,\n                result_for_llm=str(result),\n                ui_component=UiComponent(\n                    rich_component=CardComponent(\n                        type=ComponentType.CARD,\n                        title=\"Calculator Result\",\n                        content=expression,\n                    ),\n                    simple_component=SimpleTextComponent(text=expression),\n                ),\n                error=None,\n            )\n\n        except Exception as e:\n            message = str(e)\n            await asyncio.sleep(3)\n            return ToolResult(\n                success=False,\n                result_for_llm=message,\n                ui_component=UiComponent(\n                    rich_component=NotificationComponent(\n                        type=ComponentType.NOTIFICATION,\n                        level=\"error\",\n                        message=message,\n                    ),\n                    simple_component=SimpleTextComponent(text=message),\n                ),\n                error=message,\n            )\n\n\nclass MockCalculatorLlmService(LlmService):\n    \"\"\"LLM service that exercises the calculator tool before echoing the result.\"\"\"\n\n    def __init__(self, seed: Optional[int] = None):\n        self._random = random.Random(seed)\n\n    async def send_request(self, request: LlmRequest) -> LlmResponse:\n        \"\"\"Handle non-streaming calculator interactions.\"\"\"\n        await asyncio.sleep(0.05)\n        return self._build_response(request)\n\n    async def stream_request(\n        self, request: LlmRequest\n    ) -> AsyncGenerator[LlmStreamChunk, None]:\n        \"\"\"Provide streaming compatibility by yielding a single chunk.\"\"\"\n        await asyncio.sleep(0.05)\n        response = self._build_response(request)\n\n        if response.tool_calls:\n            yield LlmStreamChunk(tool_calls=response.tool_calls)\n        if response.content is not None:\n            yield LlmStreamChunk(\n                content=response.content, finish_reason=response.finish_reason\n            )\n        else:\n            yield LlmStreamChunk(finish_reason=response.finish_reason)\n\n    async def validate_tools(self, tools: List[ToolSchema]) -> List[str]:\n        \"\"\"Mock validation - no errors.\"\"\"\n        return []\n\n    def _build_response(self, request: LlmRequest) -> LlmResponse:\n        \"\"\"Create a response that either calls the tool or echoes its result.\"\"\"\n        last_message = request.messages[-1] if request.messages else None\n\n        if last_message and last_message.role == \"tool\":\n            answer = last_message.content or \"No result provided\"\n            return LlmResponse(\n                content=answer,\n                finish_reason=\"stop\",\n                usage={\n                    \"prompt_tokens\": 30,\n                    \"completion_tokens\": 10,\n                    \"total_tokens\": 40,\n                },\n            )\n\n        operation, a, b = self._random_operands()\n        tool_call = ToolCall(\n            id=f\"call_{uuid.uuid4().hex[:8]}\",\n            name=\"calculator\",\n            arguments={\"operation\": operation, \"a\": a, \"b\": b},\n        )\n\n        return LlmResponse(\n            content=\"Let me ask my calculator friend for help...\",\n            tool_calls=[tool_call],\n            finish_reason=\"tool_calls\",\n            usage={\"prompt_tokens\": 30, \"completion_tokens\": 5, \"total_tokens\": 35},\n        )\n\n    def _random_operands(self) -> Tuple[str, float, float]:\n        \"\"\"Generate operation and operands suited for the calculator tool.\"\"\"\n        operation = self._random.choice([\"add\", \"subtract\", \"multiply\", \"divide\"])\n\n        if operation == \"divide\":\n            b = float(self._random.randint(1, 10))\n            multiplier = self._random.randint(1, 10)\n            a = float(b * multiplier)\n        elif operation == \"subtract\":\n            b = float(self._random.randint(1, 10))\n            a = b + float(self._random.randint(0, 10))\n        else:\n            a = float(self._random.randint(1, 12))\n            b = float(self._random.randint(1, 12))\n\n        return operation, a, b\n\n\ndef create_demo_agent() -> Agent:\n    \"\"\"Create a demo agent with custom calculator tool.\n\n    Returns:\n        Configured Agent with calculator tool and mock calculator LLM\n    \"\"\"\n    tool_registry = ToolRegistry()\n    calculator_tool = CalculatorTool()\n    tool_registry.register(calculator_tool)\n\n    llm_service = MockCalculatorLlmService()\n\n    return Agent(\n        llm_service=llm_service,\n        tool_registry=tool_registry,\n        config=AgentConfig(\n            stream_responses=False,\n            include_thinking_indicators=False,\n        ),\n    )\n\n\nasync def main() -> None:\n    \"\"\"Run the mock custom tool example.\"\"\"\n\n    # Create agent using factory function\n    agent = create_demo_agent()\n    tool_registry = agent.tool_registry\n\n    # Create a test user\n    user = User(id=\"user123\", username=\"testuser\", permissions=[])\n\n    # Test the tool directly\n    print(\"Testing calculator tool directly:\")\n    tool_call = ToolCall(\n        id=\"test123\", name=\"calculator\", arguments={\"operation\": \"add\", \"a\": 5, \"b\": 3}\n    )\n\n    context = ToolContext(user=user, conversation_id=\"test\", request_id=\"test\")\n\n    result = await tool_registry.execute(tool_call, context)\n    print(f\"5 + 3 = {result.result_for_llm if result.success else result.error}\")\n\n    # Show available tools\n    schemas = await tool_registry.get_schemas(user)\n    print(f\"\\nAvailable tools for user: {[schema.name for schema in schemas]}\")\n\n    # Demonstrate the mock LLM triggering a tool call\n    print(\"\\nAgent conversation demo:\")\n    conversation_id = \"calc-demo\"\n    async for component in agent.send_message(\n        user=user,\n        message=\"Can you compute something for me?\",\n        conversation_id=conversation_id,\n    ):\n        print(f\"- Component type: {component.rich_component.type}\")\n        if (\n            hasattr(component.rich_component, \"content\")\n            and component.rich_component.content\n        ):\n            print(f\"Assistant: {component.rich_component.content}\")\n        elif component.simple_component and hasattr(component.simple_component, \"text\"):\n            print(f\"Assistant: {component.simple_component.text}\")\n        else:\n            print(f\"- Component data: {component.rich_component.data}\")\n\n\ndef run_interactive() -> None:\n    \"\"\"Entry point for interactive usage.\"\"\"\n    print(\"Starting mock custom tool example...\")\n    asyncio.run(main())\n\n\nif __name__ == \"__main__\":\n    run_interactive()\n"
  },
  {
    "path": "src/vanna/examples/mock_quickstart.py",
    "content": "\"\"\"\nMock quickstart example for the Vanna Agents framework.\n\nThis example shows how to create a basic agent with a mock LLM service\nand have a simple conversation.\n\nUsage:\n  Template: Copy this file and modify for your needs\n  Interactive: python -m vanna.examples.mock_quickstart\n  REPL: from vanna.examples.mock_quickstart import create_demo_agent\n  Server: python -m vanna.servers --example mock_quickstart\n\"\"\"\n\nimport asyncio\n\nfrom vanna import (\n    AgentConfig,\n    Agent,\n    MemoryConversationStore,\n    MockLlmService,\n    User,\n)\n\n\ndef create_demo_agent() -> Agent:\n    \"\"\"Create a demo agent for REPL and server usage.\n\n    Returns:\n        Configured Agent instance\n    \"\"\"\n    llm_service = MockLlmService(\n        response_content=\"Hello! I'm a helpful AI assistant created using the Vanna Agents framework.\"\n    )\n\n    return Agent(\n        llm_service=llm_service,\n        config=AgentConfig(\n            stream_responses=True,  # Enable streaming for better server experience\n            include_thinking_indicators=True,\n        ),\n    )\n\n\nasync def main() -> None:\n    \"\"\"Run the mock quickstart example.\"\"\"\n\n    # Create agent using factory function\n    agent = create_demo_agent()\n\n    # Create a test user\n    user = User(\n        id=\"user123\", username=\"testuser\", email=\"test@example.com\", permissions=[]\n    )\n\n    # Start a conversation\n    conversation_id = \"conversation123\"\n    user_message = \"Hello! Can you introduce yourself?\"\n\n    print(f\"User: {user_message}\")\n    print(\"Agent: \", end=\"\")\n\n    # Send message and collect response\n    async for component in agent.send_message(\n        user=user, message=user_message, conversation_id=conversation_id\n    ):\n        if hasattr(component, \"content\"):\n            print(component.content, end=\"\")\n\n    print()\n\n\ndef run_interactive() -> None:\n    \"\"\"Entry point for interactive usage.\"\"\"\n    print(\"Starting Vanna Agents mock quickstart demo...\")\n    asyncio.run(main())\n\n\nif __name__ == \"__main__\":\n    run_interactive()\n"
  },
  {
    "path": "src/vanna/examples/mock_quota_example.py",
    "content": "\"\"\"\nMock quota-based agent example using Mock LLM service.\n\nThis example demonstrates how to create a custom agent runner that\nenforces user-based message quotas. It shows:\n- Custom agent runner subclass\n- Quota management and enforcement\n- Error handling for quota exceeded cases\n- Multiple users with different quotas\n\nRun:\n  PYTHONPATH=. python vanna/examples/mock_quota_example.py\n\"\"\"\n\nimport asyncio\n\nfrom vanna import (\n    AgentConfig,\n    MemoryConversationStore,\n    MockLlmService,\n    User,\n)\nfrom vanna.core.registry import ToolRegistry\nfrom vanna.tools import ListFilesTool\nfrom vanna.examples.quota_agent import QuotaAgentRunner, QuotaExceededError\n\n\nasync def demonstrate_quota_system() -> None:\n    \"\"\"Demonstrate the quota-based agent system.\"\"\"\n    print(\"🚀 Starting Mock Quota-based Agent Example\\n\")\n\n    # Create a mock LLM service\n    llm_service = MockLlmService(\n        response_content=\"Hello! I'm here to help you with your questions.\"\n    )\n\n    # Create tool registry with list_files tool\n    tool_registry = ToolRegistry()\n    list_files_tool = ListFilesTool()\n    tool_registry.register(list_files_tool)\n\n    # Create conversation store\n    conversation_store = MemoryConversationStore()\n\n    # Create the quota-based agent\n    agent = QuotaAgentRunner(\n        llm_service=llm_service,\n        tool_registry=tool_registry,\n        conversation_store=conversation_store,\n        config=AgentConfig(\n            stream_responses=False,\n            include_thinking_indicators=False,\n        ),\n    )\n\n    # Create users with different quota settings\n    regular_user = User(\n        id=\"user1\", username=\"alice\", email=\"alice@example.com\", permissions=[]\n    )\n\n    premium_user = User(\n        id=\"user2\", username=\"bob\", email=\"bob@example.com\", permissions=[\"premium\"]\n    )\n\n    # Set custom quotas\n    agent.set_user_quota(regular_user.id, 3)  # Alice gets 3 messages\n    agent.set_user_quota(premium_user.id, 5)  # Bob gets 5 messages (premium)\n\n    print(\"📋 User Quotas:\")\n    print(\n        f\"  • {regular_user.username}: {agent.get_user_quota(regular_user.id)} messages\"\n    )\n    print(\n        f\"  • {premium_user.username}: {agent.get_user_quota(premium_user.id)} messages\"\n    )\n    print()\n\n    # Test regular user within quota\n    print(\"💬 Testing regular user (Alice) within quota:\")\n    for i in range(1, 4):  # Send 3 messages (within quota)\n        print(f\"  Message {i}/3:\")\n        async for component in agent.send_message(\n            user=regular_user,\n            message=f\"Hello, this is message {i}\",\n            conversation_id=\"alice-conv\",\n        ):\n            if hasattr(component, \"content\") and component.content:\n                print(f\"    Agent: {component.content}\")\n        print()\n\n    # Test regular user exceeding quota\n    print(\"⚠️  Testing regular user (Alice) exceeding quota:\")\n    async for component in agent.send_message(\n        user=regular_user,\n        message=\"This message should be blocked\",\n        conversation_id=\"alice-conv\",\n    ):\n        if hasattr(component, \"content\") and component.content:\n            print(f\"    Agent: {component.content}\")\n    print()\n\n    # Test premium user with higher quota\n    print(\"⭐ Testing premium user (Bob) with higher quota:\")\n    for i in range(1, 4):  # Send 3 messages\n        print(f\"  Message {i}/5:\")\n        async for component in agent.send_message(\n            user=premium_user,\n            message=f\"Premium user message {i}\",\n            conversation_id=\"bob-conv\",\n        ):\n            if hasattr(component, \"content\") and component.content:\n                print(f\"    Agent: {component.content}\")\n    print()\n\n    # Demonstrate quota reset\n    print(\"🔄 Resetting Alice's usage:\")\n    agent.reset_user_usage(regular_user.id)\n    print(f\"  Alice's remaining messages: {agent.get_user_remaining(regular_user.id)}\")\n    print()\n\n    print(\"✅ After reset, Alice can send messages again:\")\n    async for component in agent.send_message(\n        user=regular_user,\n        message=\"This should work after reset\",\n        conversation_id=\"alice-conv2\",\n    ):\n        if hasattr(component, \"content\") and component.content:\n            print(f\"    Agent: {component.content}\")\n\n    print(\"\\n📊 Final Usage Summary:\")\n    print(\n        f\"  • Alice: {agent.get_user_usage(regular_user.id)}/{agent.get_user_quota(regular_user.id)} used\"\n    )\n    print(\n        f\"  • Bob: {agent.get_user_usage(premium_user.id)}/{agent.get_user_quota(premium_user.id)} used\"\n    )\n\n\nasync def main() -> None:\n    \"\"\"Run the mock quota example.\"\"\"\n    await demonstrate_quota_system()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "src/vanna/examples/mock_rich_components_demo.py",
    "content": "\"\"\"\nMock rich components demonstration example.\n\nThis example shows how to create an agent that emits rich, stateful components\nincluding cards, task lists, and tool execution displays using a mock LLM service.\n\nUsage:\n  PYTHONPATH=. python vanna/examples/mock_rich_components_demo.py\n\"\"\"\n\nimport asyncio\nimport time\nfrom datetime import datetime\nfrom typing import AsyncGenerator, Optional\n\nfrom vanna import (\n    AgentConfig,\n    Agent,\n    MemoryConversationStore,\n    MockLlmService,\n    User,\n)\nfrom vanna.core.components import UiComponent\nfrom vanna.core.rich_components import (\n    StatusCardComponent,\n    ProgressDisplayComponent,\n    LogViewerComponent,\n    BadgeComponent,\n    IconTextComponent,\n    RichTextComponent,\n    Task,\n)\n\n\nclass RichComponentsAgent(Agent):\n    \"\"\"Agent that demonstrates rich component capabilities.\"\"\"\n\n    async def send_message(\n        self,\n        user: User,\n        message: str,\n        *,\n        conversation_id: Optional[str] = None,\n    ) -> AsyncGenerator[UiComponent, None]:\n        \"\"\"Send message and yield UiComponent(rich_component=rich) components.\"\"\"\n\n        # Welcome message using IconText\n        yield UiComponent(\n            rich_component=IconTextComponent(\n                id=\"welcome-message\",\n                icon=\"👋\",\n                text=f\"Hello {user.username}! I'll demonstrate primitive components.\",\n                variant=\"primary\",\n                size=\"large\",\n            )\n        )\n\n        # Status card showing we're processing\n        status_card = StatusCardComponent(\n            id=\"processing-status\",\n            title=\"Processing Request\",\n            status=\"running\",\n            description=\"Processing your request...\",\n            icon=\"⚙️\",\n        )\n        yield UiComponent(rich_component=status_card)\n\n        # Simulate some processing time\n        await asyncio.sleep(1)\n\n        # Update status to success\n        yield UiComponent(\n            rich_component=status_card.set_status(\n                \"success\", \"Request processed successfully!\"\n            )\n        )\n\n        # Create a status card for overall demo progress\n        demo_card = StatusCardComponent(\n            id=\"demo-progress\",\n            title=\"Demo Progress\",\n            status=\"running\",\n            description=\"Starting primitive components demonstration...\",\n            icon=\"🎯\",\n        )\n        yield UiComponent(rich_component=demo_card)\n\n        # Create badges for different stages\n        stages = [\n            (\"Initialize\", \"success\", \"✅\"),\n            (\"Components\", \"running\", \"⚙️\"),\n            (\"Progress\", \"pending\", \"⏳\"),\n            (\"Logs\", \"pending\", \"📋\"),\n            (\"Complete\", \"pending\", \"🎉\"),\n        ]\n\n        for stage_name, stage_status, stage_icon in stages:\n            yield UiComponent(\n                rich_component=BadgeComponent(\n                    id=f\"stage-{stage_name.lower()}\",\n                    text=stage_name,\n                    variant=stage_status if stage_status != \"pending\" else \"default\",\n                    icon=stage_icon,\n                    size=\"md\",\n                )\n            )\n\n        # Progress display\n        progress_display = ProgressDisplayComponent(\n            id=\"demo-progress-bar\",\n            label=\"Overall Progress\",\n            value=0.2,\n            description=\"Initializing demonstration...\",\n            status=\"info\",\n            animated=True,\n        )\n        yield UiComponent(rich_component=progress_display)\n\n        # Create log viewer for detailed progress\n        log_viewer = LogViewerComponent(id=\"demo-logs\", title=\"Demo Activity Log\")\n        yield UiComponent(rich_component=log_viewer)\n\n        # Simulate work with updates\n        for i in range(3):\n            await asyncio.sleep(1)\n\n            # Update progress\n            progress_value = 0.2 + (i + 1) * 0.2\n            step_name = [\"Creating components\", \"Updating progress\", \"Finalizing demo\"][\n                i\n            ]\n\n            yield UiComponent(\n                rich_component=progress_display.update_progress(\n                    progress_value, f\"Step {i + 2} of 5: {step_name}...\"\n                )\n            )\n\n            # Update demo card\n            yield UiComponent(\n                rich_component=demo_card.set_status(\n                    \"running\",\n                    f\"Step {i + 2} of 5 completed. Progress: {int(progress_value * 100)}%\",\n                )\n            )\n\n            # Add log entry\n            yield UiComponent(\n                rich_component=log_viewer.add_entry(\n                    f\"Completed step: {step_name}\", \"info\"\n                )\n            )\n\n            # Update stage badges\n            if i == 0:\n                yield UiComponent(\n                    rich_component=BadgeComponent(\n                        id=\"stage-components\",\n                        text=\"Components\",\n                        variant=\"success\",\n                        icon=\"✅\",\n                        size=\"md\",\n                    )\n                )\n            elif i == 1:\n                yield UiComponent(\n                    rich_component=BadgeComponent(\n                        id=\"stage-progress\",\n                        text=\"Progress\",\n                        variant=\"success\",\n                        icon=\"✅\",\n                        size=\"md\",\n                    )\n                )\n                yield UiComponent(\n                    rich_component=BadgeComponent(\n                        id=\"stage-logs\",\n                        text=\"Logs\",\n                        variant=\"running\",\n                        icon=\"📋\",\n                        size=\"md\",\n                    )\n                )\n\n        # Tool execution using primitive components\n        tool_status = StatusCardComponent(\n            id=\"demo-tool\",\n            title=\"Analyze Data Tool\",\n            status=\"running\",\n            description=\"Running regression analysis on user_data.csv\",\n            icon=\"🔬\",\n        )\n        yield UiComponent(rich_component=tool_status)\n\n        # Tool progress\n        tool_progress = ProgressDisplayComponent(\n            id=\"tool-progress\",\n            label=\"Tool Execution\",\n            value=0.0,\n            description=\"Initializing tool...\",\n            animated=True,\n        )\n        yield UiComponent(rich_component=tool_progress)\n\n        # Tool logs\n        tool_logs = LogViewerComponent(id=\"tool-logs\", title=\"Tool Execution Log\")\n        yield UiComponent(rich_component=tool_logs)\n\n        # Simulate tool execution steps\n        tool_steps = [\n            (0.2, \"Loading dataset...\", \"info\"),\n            (0.4, \"Dataset loaded: 1000 rows, 5 columns\", \"info\"),\n            (0.6, \"Preprocessing data...\", \"info\"),\n            (0.8, \"Running regression analysis...\", \"info\"),\n            (1.0, \"Analysis complete!\", \"info\"),\n        ]\n\n        for progress_val, log_message, log_level in tool_steps:\n            await asyncio.sleep(0.5)\n\n            yield UiComponent(\n                rich_component=tool_progress.update_progress(\n                    progress_val, f\"Progress: {int(progress_val * 100)}%\"\n                )\n            )\n            yield UiComponent(\n                rich_component=tool_logs.add_entry(log_message, log_level)\n            )\n\n        # Complete tool execution\n        yield UiComponent(\n            rich_component=tool_status.set_status(\n                \"success\",\n                \"Tool completed successfully. R² = 0.85, strong correlation found.\",\n            )\n        )\n\n        # Show results using IconText\n        yield UiComponent(\n            rich_component=IconTextComponent(\n                id=\"tool-results\",\n                icon=\"📊\",\n                text=\"Analysis Results: R² = 0.85 (Strong correlation)\",\n                variant=\"success\",\n                size=\"medium\",\n            )\n        )\n\n        # Update final stage badge\n        yield UiComponent(\n            rich_component=BadgeComponent(\n                id=\"stage-logs\", text=\"Logs\", variant=\"success\", icon=\"✅\", size=\"md\"\n            )\n        )\n        yield UiComponent(\n            rich_component=BadgeComponent(\n                id=\"stage-complete\",\n                text=\"Complete\",\n                variant=\"success\",\n                icon=\"🎉\",\n                size=\"md\",\n            )\n        )\n\n        # Final updates\n        yield UiComponent(\n            rich_component=progress_display.update_progress(\n                1.0, \"Demo completed successfully!\"\n            )\n        )\n\n        yield UiComponent(\n            rich_component=demo_card.set_status(\n                \"success\", \"Primitive components demonstration finished successfully!\"\n            )\n        )\n\n        # Add final log entry\n        yield UiComponent(\n            rich_component=tool_logs.add_entry(\"Demo completed successfully!\", \"info\")\n        )\n\n        # Add final text response\n        yield UiComponent(\n            rich_component=RichTextComponent(\n                content=f\"\"\"## Primitive Components Demo Complete!\n\nI've demonstrated the new primitive component system:\n\n- **Status Cards**: Domain-agnostic status displays that work for any process\n- **Progress Displays**: Reusable progress indicators with animations\n- **Log Viewers**: Structured log display for any activity\n- **Badges**: Flexible status and category indicators\n- **Icon Text**: Composable icon+text combinations\n\n### Key Benefits of Primitive Components:\n\n- **Separation of Concerns**: UI components are purely presentational\n- **Reusability**: Components work across different domains and tools\n- **Composability**: Tools build exactly the UI they need from primitives\n- **Maintainability**: Business logic changes don't affect UI components\n- **Extensibility**: New tools don't require new component types\n\n**Primitive Components**: Compose UI from domain-agnostic building blocks\n**After**: Tools compose UI from primitive `StatusCard` + `ProgressDisplay` + `LogViewer`\n\nYour message was: \"{message}\"\n\"\"\",\n                markdown=True,\n            )\n        )\n\n\n# CLI compatibility alias\ncreate_demo_agent = lambda: create_rich_demo_agent()\n\n\ndef create_rich_demo_agent() -> RichComponentsAgent:\n    \"\"\"Create a primitive components demo agent.\n\n    Returns:\n        Configured RichComponentsAgent instance\n    \"\"\"\n    llm_service = MockLlmService(response_content=\"Primitive components demo response\")\n\n    return RichComponentsAgent(\n        llm_service=llm_service,\n        config=AgentConfig(\n            stream_responses=True,\n            include_thinking_indicators=False,  # We'll use custom status cards\n        ),\n    )\n\n\nasync def main() -> None:\n    \"\"\"Run the primitive components demo.\"\"\"\n\n    # Create agent\n    agent = create_rich_demo_agent()\n\n    # Create a test user\n    user = User(\n        id=\"user123\", username=\"demo_user\", email=\"demo@example.com\", permissions=[]\n    )\n\n    # Start a conversation\n    conversation_id = \"primitive_demo_123\"\n    user_message = \"Show me the primitive components demo!\"\n\n    print(f\"User: {user_message}\")\n    print(\"Agent response (primitive components):\")\n    print(\"=\" * 50)\n\n    # Send message and display components\n    component_count = 0\n    async for component in agent.send_message(\n        user=user, message=user_message, conversation_id=conversation_id\n    ):\n        component_count += 1\n        rich_comp = component.rich_component\n        component_type = getattr(rich_comp, \"type\", rich_comp.__class__.__name__)\n        component_id = getattr(rich_comp, \"id\", \"N/A\")\n        lifecycle = getattr(rich_comp, \"lifecycle\", \"N/A\")\n\n        print(\n            f\"[{component_count:2d}] {component_type} (id: {component_id[:8]}, lifecycle: {lifecycle})\"\n        )\n\n        # Show some component details\n        if hasattr(rich_comp, \"title\"):\n            print(f\"     Title: {rich_comp.title}\")\n        if hasattr(rich_comp, \"content\") and len(str(rich_comp.content)) < 100:\n            print(f\"     Content: {rich_comp.content}\")\n        if hasattr(rich_comp, \"status\"):\n            print(f\"     Status: {rich_comp.status}\")\n        if (\n            hasattr(rich_comp, \"value\")\n            and hasattr(rich_comp.type, \"value\")\n            and rich_comp.type.value == \"progress_bar\"\n        ):\n            print(f\"     Progress: {rich_comp.value:.1%}\")\n\n        print()\n\n    print(\"=\" * 50)\n    print(f\"Total components emitted: {component_count}\")\n\n\ndef run_interactive() -> None:\n    \"\"\"Entry point for interactive usage.\"\"\"\n    print(\"Starting Primitive Components Demo...\")\n    asyncio.run(main())\n\n\nif __name__ == \"__main__\":\n    run_interactive()\n"
  },
  {
    "path": "src/vanna/examples/mock_sqlite_example.py",
    "content": "\"\"\"\nMock example showing how to use the SQL query tool with the Chinook database.\n\nThis example demonstrates using the RunSqlTool with SqliteRunner and a mock LLM service\nthat automatically executes sample SQL queries against the Chinook database.\n\nUsage:\n  Template: Copy this file and modify for your custom database\n  Interactive: python -m vanna.examples.mock_sqlite_example\n  REPL: from vanna.examples.mock_sqlite_example import create_demo_agent\n  Server: python -m vanna.servers --example mock_sqlite_example\n\"\"\"\n\nimport asyncio\nimport os\nimport random\nimport uuid\nfrom typing import AsyncGenerator, Dict, List, Optional, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom vanna import (\n    AgentConfig,\n    Agent,\n    Tool,\n    ToolContext,\n    ToolRegistry,\n    ToolResult,\n    User,\n    UiComponent,\n)\nfrom vanna.core.interfaces import LlmService\nfrom vanna.core.models import (\n    LlmRequest,\n    LlmResponse,\n    LlmStreamChunk,\n    ToolCall,\n    ToolSchema,\n)\nfrom vanna.core.rich_components import (\n    CardComponent,\n    NotificationComponent,\n    ComponentType,\n)\nfrom vanna.core.simple_components import (\n    SimpleTextComponent,\n)\nfrom vanna.tools import RunSqlTool\nfrom vanna.integrations.sqlite import SqliteRunner\n\n\nclass MockSqliteLlmService(LlmService):\n    \"\"\"LLM service that exercises the SQLite query tool with sample queries.\"\"\"\n\n    def __init__(self, seed: Optional[int] = None):\n        self._random = random.Random(seed)\n        self._sample_queries = [\n            \"SELECT name FROM sqlite_master WHERE type='table'\",\n            \"SELECT COUNT(*) as total_customers FROM Customer\",\n            \"SELECT FirstName, LastName FROM Customer LIMIT 5\",\n            \"SELECT Name, Composer FROM Track WHERE Composer IS NOT NULL LIMIT 5\",\n            \"SELECT COUNT(*) as album_count FROM Album\",\n            \"SELECT Name FROM Artist LIMIT 10\",\n            \"SELECT AVG(Total) as avg_invoice_total FROM Invoice\",\n            \"SELECT GenreId, COUNT(*) as track_count FROM Track GROUP BY GenreId LIMIT 5\",\n        ]\n\n    async def send_request(self, request: LlmRequest) -> LlmResponse:\n        \"\"\"Handle non-streaming SQLite interactions.\"\"\"\n        await asyncio.sleep(0.1)\n        return self._build_response(request)\n\n    async def stream_request(\n        self, request: LlmRequest\n    ) -> AsyncGenerator[LlmStreamChunk, None]:\n        \"\"\"Provide streaming compatibility by yielding a single chunk.\"\"\"\n        await asyncio.sleep(0.1)\n        response = self._build_response(request)\n\n        if response.tool_calls:\n            yield LlmStreamChunk(tool_calls=response.tool_calls)\n        if response.content is not None:\n            yield LlmStreamChunk(\n                content=response.content, finish_reason=response.finish_reason\n            )\n        else:\n            yield LlmStreamChunk(finish_reason=response.finish_reason)\n\n    async def validate_tools(self, tools: List[ToolSchema]) -> List[str]:\n        \"\"\"Mock validation - no errors.\"\"\"\n        return []\n\n    def _build_response(self, request: LlmRequest) -> LlmResponse:\n        \"\"\"Create a response that either calls the tool or explains its result.\"\"\"\n        last_message = request.messages[-1] if request.messages else None\n\n        if last_message and last_message.role == \"tool\":\n            # Respond to tool result\n            result = last_message.content or \"No result provided\"\n            return LlmResponse(\n                content=f\"Here's what I found in the database:\\n\\n{result}\",\n                finish_reason=\"stop\",\n                usage={\n                    \"prompt_tokens\": 40,\n                    \"completion_tokens\": 20,\n                    \"total_tokens\": 60,\n                },\n            )\n\n        # Generate a random SQL query\n        sql_query = self._random.choice(self._sample_queries)\n        tool_call = ToolCall(\n            id=f\"call_{uuid.uuid4().hex[:8]}\",\n            name=\"run_sql\",\n            arguments={\"sql\": sql_query},\n        )\n\n        return LlmResponse(\n            content=\"Let me query the Chinook database for you...\",\n            tool_calls=[tool_call],\n            finish_reason=\"tool_calls\",\n            usage={\"prompt_tokens\": 30, \"completion_tokens\": 10, \"total_tokens\": 40},\n        )\n\n\ndef create_demo_agent() -> Agent:\n    \"\"\"Create a demo agent with SQLite query tool.\n\n    Returns:\n        Configured Agent with SQLite tool and mock LLM\n    \"\"\"\n    # Get the path to the Chinook database\n    database_path = os.path.join(\n        os.path.dirname(__file__), \"..\", \"..\", \"Chinook.sqlite\"\n    )\n    database_path = os.path.abspath(database_path)\n\n    if not os.path.exists(database_path):\n        raise FileNotFoundError(\n            f\"Chinook database not found at {database_path}. Please download it from https://vanna.ai/Chinook.sqlite\"\n        )\n\n    tool_registry = ToolRegistry()\n    sqlite_runner = SqliteRunner(database_path=database_path)\n    sql_tool = RunSqlTool(sql_runner=sqlite_runner)\n    tool_registry.register(sql_tool)\n\n    llm_service = MockSqliteLlmService()\n\n    return Agent(\n        llm_service=llm_service,\n        tool_registry=tool_registry,\n        config=AgentConfig(\n            stream_responses=False,\n            include_thinking_indicators=False,\n        ),\n    )\n\n\nasync def main() -> None:\n    \"\"\"Run the mock SQLite example.\"\"\"\n\n    # Create agent using factory function\n    agent = create_demo_agent()\n    tool_registry = agent.tool_registry\n\n    # Create a test user\n    user = User(id=\"user123\", username=\"testuser\", permissions=[])\n\n    # Test the tool directly\n    print(\"Testing SQL tool directly:\")\n    tool_call = ToolCall(\n        id=\"test123\",\n        name=\"run_sql\",\n        arguments={\"sql\": \"SELECT name FROM sqlite_master WHERE type='table'\"},\n    )\n\n    context = ToolContext(user=user, conversation_id=\"test\", request_id=\"test\")\n\n    result = await tool_registry.execute(tool_call, context)\n    print(\n        f\"Tables in database:\\n{result.result_for_llm if result.success else result.error}\"\n    )\n\n    # Show available tools\n    schemas = await tool_registry.get_schemas(user)\n    print(f\"\\nAvailable tools for user: {[schema.name for schema in schemas]}\")\n\n    # Demonstrate the mock LLM triggering SQL queries\n    print(\"\\n\" + \"=\" * 50)\n    print(\"Agent conversation demo:\")\n    print(\"=\" * 50)\n\n    conversation_id = \"sqlite-demo\"\n\n    # Run multiple queries to show different results\n    for i in range(3):\n        print(f\"\\n--- Query {i + 1} ---\")\n        async for component in agent.send_message(\n            user=user,\n            message=f\"Show me some data from the database (query {i + 1})\",\n            conversation_id=conversation_id,\n        ):\n            if (\n                hasattr(component.rich_component, \"content\")\n                and component.rich_component.content\n            ):\n                print(f\"Assistant: {component.rich_component.content}\")\n            elif component.simple_component and hasattr(\n                component.simple_component, \"text\"\n            ):\n                print(f\"Assistant: {component.simple_component.text}\")\n\n\ndef run_interactive() -> None:\n    \"\"\"Entry point for interactive usage.\"\"\"\n    print(\"Starting mock SQLite example...\")\n    print(\"This example uses the Chinook database to demonstrate SQL queries.\")\n    asyncio.run(main())\n\n\nif __name__ == \"__main__\":\n    run_interactive()\n"
  },
  {
    "path": "src/vanna/examples/openai_quickstart.py",
    "content": "\"\"\"\nOpenAI example using OpenAILlmService.\n\nLoads environment from .env (via python-dotenv), uses model 'gpt-5' by default,\nand sends a simple message through a Agent.\n\nRun:\n  PYTHONPATH=. python vanna/examples/openai_quickstart.py\n\"\"\"\n\nimport asyncio\nimport importlib.util\nimport os\nimport sys\n\n\ndef ensure_env() -> None:\n    if importlib.util.find_spec(\"dotenv\") is not None:\n        from dotenv import load_dotenv\n\n        # Load from local .env without overriding existing env\n        load_dotenv(dotenv_path=os.path.join(os.getcwd(), \".env\"), override=False)\n    else:\n        print(\n            \"[warn] python-dotenv not installed; skipping .env load. Install with: pip install python-dotenv\"\n        )\n\n    if not os.getenv(\"OPENAI_API_KEY\"):\n        print(\n            \"[error] OPENAI_API_KEY is not set. Add it to your environment or .env file.\"\n        )\n        sys.exit(1)\n\n\nasync def main() -> None:\n    ensure_env()\n\n    # Lazy import after env load to allow custom base_url/org via env\n    try:\n        from vanna.integrations.anthropic import OpenAILlmService\n    except ImportError as e:\n        print(\n            \"[error] openai extra not installed. Install with: pip install -e .[openai]\"\n        )\n        raise\n\n    from vanna import AgentConfig, Agent, User\n    from vanna.core.registry import ToolRegistry\n    from vanna.tools import ListFilesTool\n\n    # Default to 'gpt-5' for this demo; override via $OPENAI_MODEL if desired\n    model = os.getenv(\"OPENAI_MODEL\", \"gpt-5\")\n    print(f\"Using OpenAI model: {model}\")\n\n    llm = OpenAILlmService(model=model)\n\n    # Create tool registry and register the list_files tool\n    tool_registry = ToolRegistry()\n    list_files_tool = ListFilesTool()\n    tool_registry.register(list_files_tool)\n\n    # Some models (e.g., reasoning/gpt-5) only support the default temperature=1.0\n    agent = Agent(\n        llm_service=llm,\n        config=AgentConfig(stream_responses=False, temperature=1.0),\n        tool_registry=tool_registry,\n    )\n\n    user = User(id=\"demo-user\", username=\"demo\")\n    conversation_id = \"openai-demo\"\n\n    print(\"Sending: 'List the files in the current directory'\\n\")\n    async for component in agent.send_message(\n        user=user,\n        message=\"List the files in the current directory\",\n        conversation_id=conversation_id,\n    ):\n        if hasattr(component, \"content\") and component.content:\n            print(\"Assistant:\", component.content)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "src/vanna/examples/primitive_components_demo.py",
    "content": "\"\"\"\nDemonstration of the new primitive component system.\n\nThis example shows how tools compose UI from primitive, domain-agnostic\ncomponents like StatusCardComponent, ProgressDisplayComponent, etc.\n\nUsage:\n  PYTHONPATH=. python vanna/examples/primitive_components_demo.py\n\"\"\"\n\nimport asyncio\nimport uuid\nfrom datetime import datetime\nfrom typing import AsyncGenerator, Optional\n\nfrom vanna import (\n    AgentConfig,\n    Agent,\n    MemoryConversationStore,\n    MockLlmService,\n    User,\n)\nfrom vanna.core.components import UiComponent\nfrom vanna.core.rich_components import (\n    StatusCardComponent,\n    ProgressDisplayComponent,\n    LogViewerComponent,\n    BadgeComponent,\n    IconTextComponent,\n    RichTextComponent,\n)\n\n\nclass PrimitiveComponentsAgent(Agent):\n    \"\"\"Agent that demonstrates the new primitive component system.\"\"\"\n\n    async def send_message(\n        self,\n        user: User,\n        message: str,\n        *,\n        conversation_id: Optional[str] = None,\n    ) -> AsyncGenerator[UiComponent, None]:\n        \"\"\"Send message and demonstrate primitive component composition.\"\"\"\n\n        session_id = str(uuid.uuid4())[:8]\n\n        # Demo 1: Tool execution using primitive components\n        yield UiComponent(\n            rich_component=RichTextComponent(\n                content=\"## Primitive Components Demo\\n\\nShowing how tools now compose UI from primitive components:\",\n                markdown=True,\n            )\n        )\n\n        # Status card for overall operation\n        operation_status = StatusCardComponent(\n            id=f\"operation-{session_id}\",\n            title=\"Data Analysis Pipeline\",\n            status=\"running\",\n            description=\"Processing user data through multiple analysis stages\",\n            icon=\"⚙️\",\n        )\n        yield UiComponent(rich_component=operation_status)\n\n        # Progress display for overall progress\n        overall_progress = ProgressDisplayComponent(\n            id=f\"progress-{session_id}\",\n            label=\"Overall Progress\",\n            value=0.0,\n            description=\"Starting analysis...\",\n            animated=True,\n        )\n        yield UiComponent(rich_component=overall_progress)\n\n        # Log viewer for detailed output\n        log_viewer = LogViewerComponent(\n            id=f\"logs-{session_id}\",\n            title=\"Analysis Log\",\n            entries=[],\n            show_timestamps=True,\n            auto_scroll=True,\n        )\n        yield UiComponent(rich_component=log_viewer)\n\n        # Simulate analysis stages\n        stages = [\n            (\"Data Loading\", \"📊\", 0.2),\n            (\"Data Validation\", \"✅\", 0.4),\n            (\"Statistical Analysis\", \"🧮\", 0.6),\n            (\"Report Generation\", \"📄\", 0.8),\n            (\"Finalization\", \"🎯\", 1.0),\n        ]\n\n        for i, (stage_name, stage_icon, progress_value) in enumerate(stages):\n            await asyncio.sleep(0.8)\n\n            # Update overall status\n            status = \"success\" if progress_value == 1.0 else \"running\"\n            yield UiComponent(\n                rich_component=operation_status.set_status(\n                    status, f\"Executing: {stage_name}\"\n                )\n            )\n\n            # Update progress\n            yield UiComponent(\n                rich_component=overall_progress.update_progress(\n                    progress_value, f\"Executing {stage_name}...\"\n                )\n            )\n\n            # Add log entry\n            yield UiComponent(\n                rich_component=log_viewer.add_entry(f\"Starting {stage_name}\", \"info\")\n            )\n\n            # Create a status card for this specific stage\n            stage_status = StatusCardComponent(\n                id=f\"stage-{i}-{session_id}\",\n                title=stage_name,\n                status=\"running\" if progress_value < 1.0 else \"success\",\n                description=f\"Processing stage {i + 1} of {len(stages)}\",\n                icon=stage_icon,\n            )\n            yield UiComponent(rich_component=stage_status)\n\n            await asyncio.sleep(0.5)\n\n            # Complete the stage\n            final_stage_status = \"success\" if progress_value < 1.0 else \"completed\"\n            yield UiComponent(\n                rich_component=stage_status.set_status(\n                    final_stage_status, f\"{stage_name} completed successfully\"\n                )\n            )\n            yield UiComponent(\n                rich_component=log_viewer.add_entry(f\"Completed {stage_name}\", \"info\")\n            )\n\n        # Demo 2: Badge and IconText primitives\n        yield UiComponent(\n            rich_component=RichTextComponent(\n                content=\"\\n### Primitive Component Examples\\n\\nShowing individual primitive components:\",\n                markdown=True,\n            )\n        )\n\n        # Various badge examples\n        badges = [\n            BadgeComponent(text=\"Processing\", variant=\"primary\", size=\"small\"),\n            BadgeComponent(text=\"Complete\", variant=\"success\", size=\"medium\"),\n            BadgeComponent(text=\"Warning\", variant=\"warning\", size=\"large\", icon=\"⚠️\"),\n            BadgeComponent(text=\"Error\", variant=\"error\", size=\"medium\", icon=\"❌\"),\n        ]\n\n        for badge in badges:\n            yield UiComponent(rich_component=badge)\n\n        # IconText examples\n        icon_texts = [\n            IconTextComponent(\n                icon=\"📊\",\n                text=\"Data Analysis Complete\",\n                variant=\"primary\",\n                size=\"large\",\n            ),\n            IconTextComponent(\n                icon=\"✅\", text=\"All tests passed\", variant=\"default\", size=\"medium\"\n            ),\n            IconTextComponent(\n                icon=\"⏱️\",\n                text=\"Processing time: 2.3s\",\n                variant=\"secondary\",\n                size=\"small\",\n            ),\n        ]\n\n        for icon_text in icon_texts:\n            yield UiComponent(rich_component=icon_text)\n\n        # Demo 3: Comparison with old approach\n        yield UiComponent(\n            rich_component=RichTextComponent(\n                content=f\"\"\"\n## Key Benefits of Primitive Components\n\n**Primitive Component Approach:**\n```python\n# Tool composes UI from primitives\nstatus_card = StatusCardComponent(\n    title=\"Data Analysis\",\n    status=\"running\",  # Pure UI state\n    icon=\"📊\"\n)\nprogress = ProgressDisplayComponent(\n    label=\"Analysis Progress\",\n    value=0.5\n)\nlogs = LogViewerComponent(\n    title=\"Analysis Log\",\n    entries=log_entries\n)\n```\n\n### Benefits:\n- **Separation of Concerns**: UI components are purely presentational\n- **Reusability**: Status cards work for any process, not just tools\n- **Composability**: Tools build exactly the UI they need\n- **Maintainability**: Changes to business logic don't affect UI components\n- **Extensibility**: New tools don't require new component types\n\nYour message was: \"{message}\"\n\"\"\",\n                markdown=True,\n            )\n        )\n\n\ndef create_primitive_demo_agent() -> PrimitiveComponentsAgent:\n    \"\"\"Create a primitive components demo agent.\n\n    Returns:\n        Configured PrimitiveComponentsAgent instance\n    \"\"\"\n    llm_service = MockLlmService(response_content=\"Primitive components demo response\")\n\n    return PrimitiveComponentsAgent(\n        llm_service=llm_service,\n        config=AgentConfig(\n            stream_responses=True,\n            include_thinking_indicators=False,\n        ),\n    )\n\n\nasync def main() -> None:\n    \"\"\"Run the primitive components demo.\"\"\"\n\n    # Create agent\n    agent = create_primitive_demo_agent()\n\n    # Create a test user\n    user = User(\n        id=\"user123\", username=\"demo_user\", email=\"demo@example.com\", permissions=[]\n    )\n\n    # Start a conversation\n    conversation_id = \"primitive_demo_123\"\n    user_message = \"Show me how the new primitive component system works!\"\n\n    print(f\"User: {user_message}\")\n    print(\"Agent response (primitive components):\")\n    print(\"=\" * 60)\n\n    # Send message and display components\n    component_count = 0\n    async for component in agent.send_message(\n        user=user, message=user_message, conversation_id=conversation_id\n    ):\n        component_count += 1\n        component_type = getattr(component, \"type\", component.__class__.__name__)\n        component_id = getattr(component, \"id\", \"N/A\")\n\n        print(\n            f\"[{component_count:2d}] {component_type.value if hasattr(component_type, 'value') else component_type} (id: {component_id[:12] if len(str(component_id)) > 12 else component_id})\"\n        )\n\n        rich_comp = component.rich_component\n\n        # Show component details\n        if hasattr(rich_comp, \"title\"):\n            print(f\"     Title: {rich_comp.title}\")\n        if hasattr(rich_comp, \"status\"):\n            print(f\"     Status: {rich_comp.status}\")\n        if hasattr(rich_comp, \"description\") and rich_comp.description:\n            desc = (\n                rich_comp.description[:60] + \"...\"\n                if len(rich_comp.description) > 60\n                else rich_comp.description\n            )\n            print(f\"     Description: {desc}\")\n        if (\n            hasattr(rich_comp, \"value\")\n            and hasattr(rich_comp.type, \"value\")\n            and rich_comp.type.value == \"progress_display\"\n        ):\n            print(f\"     Progress: {rich_comp.value:.1%}\")\n\n        print()\n\n    print(\"=\" * 60)\n    print(f\"Total components emitted: {component_count}\")\n    print(\"\\nThis demonstrates how tools can now compose rich UIs\")\n    print(\"from primitive, reusable components without semantic coupling!\")\n\n\ndef run_interactive() -> None:\n    \"\"\"Entry point for interactive usage.\"\"\"\n    print(\"Starting Primitive Components Demo...\")\n    asyncio.run(main())\n\n\nif __name__ == \"__main__\":\n    run_interactive()\n"
  },
  {
    "path": "src/vanna/examples/quota_lifecycle_example.py",
    "content": "\"\"\"\nExample demonstrating lifecycle hooks for user quota management.\n\nThis example shows how to use lifecycle hooks to add custom functionality\nlike quota management without creating custom agent runner subclasses.\n\"\"\"\n\nfrom typing import Any, Dict, Optional\nfrom vanna.core import Agent, LifecycleHook, User\nfrom vanna.core.errors import AgentError\n\n\nclass QuotaExceededError(AgentError):\n    \"\"\"Raised when a user exceeds their message quota.\"\"\"\n\n    pass\n\n\nclass QuotaCheckHook(LifecycleHook):\n    \"\"\"Lifecycle hook that enforces user-based message quotas.\"\"\"\n\n    def __init__(self, default_quota: int = 10) -> None:\n        \"\"\"Initialize quota hook.\n\n        Args:\n            default_quota: Default quota per user if not specifically set\n        \"\"\"\n        self._user_quotas: Dict[str, int] = {}\n        self._user_usage: Dict[str, int] = {}\n        self._default_quota = default_quota\n\n    def set_user_quota(self, user_id: str, quota: int) -> None:\n        \"\"\"Set a specific quota for a user.\"\"\"\n        self._user_quotas[user_id] = quota\n\n    def get_user_quota(self, user_id: str) -> int:\n        \"\"\"Get the quota for a user.\"\"\"\n        return self._user_quotas.get(user_id, self._default_quota)\n\n    def get_user_usage(self, user_id: str) -> int:\n        \"\"\"Get current usage count for a user.\"\"\"\n        return self._user_usage.get(user_id, 0)\n\n    def get_user_remaining(self, user_id: str) -> int:\n        \"\"\"Get remaining messages for a user.\"\"\"\n        return self.get_user_quota(user_id) - self.get_user_usage(user_id)\n\n    def reset_user_usage(self, user_id: str) -> None:\n        \"\"\"Reset usage count for a user.\"\"\"\n        self._user_usage[user_id] = 0\n\n    async def before_message(self, user: User, message: str) -> Optional[str]:\n        \"\"\"Check quota before processing message.\n\n        Raises:\n            QuotaExceededError: If user has exceeded their quota\n        \"\"\"\n        usage = self.get_user_usage(user.id)\n        quota = self.get_user_quota(user.id)\n\n        if usage >= quota:\n            raise QuotaExceededError(\n                f\"User {user.username} has exceeded their quota of {quota} messages. \"\n                f\"Current usage: {usage}\"\n            )\n\n        # Increment usage count\n        current_usage = self._user_usage.get(user.id, 0)\n        self._user_usage[user.id] = current_usage + 1\n\n        # Don't modify the message\n        return None\n\n\nclass LoggingHook(LifecycleHook):\n    \"\"\"Example logging hook for demonstration.\"\"\"\n\n    async def before_message(self, user: User, message: str) -> Optional[str]:\n        \"\"\"Log incoming messages.\"\"\"\n        print(f\"[LOG] User {user.username} ({user.id}) sent message: {message[:50]}...\")\n        return None\n\n    async def after_message(self, result: Any) -> None:\n        \"\"\"Log message completion.\"\"\"\n        print(f\"[LOG] Message processing completed\")\n\n\nasync def run_example() -> None:\n    \"\"\"\n    Example showing how to use lifecycle hooks with Agent.\n\n    Instead of creating a custom subclass, we compose\n    the behavior using lifecycle hooks.\n    \"\"\"\n    from vanna.core.registry import ToolRegistry\n    from vanna.integrations.anthropic import AnthropicLlmService\n    from vanna.integrations.local import MemoryConversationStore\n\n    # Create quota hook\n    quota_hook = QuotaCheckHook(default_quota=10)\n    quota_hook.set_user_quota(\"user123\", 5)  # Set custom quota for specific user\n\n    # Create logging hook\n    logging_hook = LoggingHook()\n\n    # Create agent with multiple hooks\n    agent = Agent(\n        llm_service=AnthropicLlmService(api_key=\"your-api-key\"),\n        tool_registry=ToolRegistry(),\n        conversation_store=MemoryConversationStore(),\n        lifecycle_hooks=[\n            logging_hook,  # Logs will happen first\n            quota_hook,  # Then quota check\n        ],\n    )\n\n    # Create a test user\n    user = User(\n        id=\"user123\", username=\"test_user\", email=\"test@example.com\", permissions=[]\n    )\n\n    # Send messages - will track quota\n    try:\n        async for component in agent.send_message(user=user, message=\"Hello, agent!\"):\n            # Process UI components\n            pass\n\n        # Check remaining quota\n        remaining = quota_hook.get_user_remaining(user.id)\n        print(f\"Remaining messages: {remaining}/{quota_hook.get_user_quota(user.id)}\")\n\n    except QuotaExceededError as e:\n        print(f\"Quota exceeded: {e}\")\n\n\nif __name__ == \"__main__\":\n    import asyncio\n\n    asyncio.run(run_example())\n"
  },
  {
    "path": "src/vanna/examples/visualization_example.py",
    "content": "\"\"\"\nExample demonstrating SQL query execution with automatic visualization.\n\nThis example shows the integration of RunSqlTool and VisualizeDataTool,\ndemonstrating how SQL results are saved to CSV files and can be visualized\nusing the visualization tool with dependency injection.\n\nUsage:\n  PYTHONPATH=. python vanna/examples/visualization_example.py\n\"\"\"\n\nimport asyncio\nimport os\nimport sys\nimport uuid\nfrom typing import AsyncGenerator, List, Optional\n\nfrom vanna import (\n    AgentConfig,\n    Agent,\n    ToolRegistry,\n    User,\n)\nfrom vanna.core import LlmService\nfrom vanna.core import (\n    LlmRequest,\n    LlmResponse,\n    LlmStreamChunk,\n    ToolCall,\n    ToolSchema,\n)\nfrom vanna.integrations.sqlite import SqliteRunner\nfrom vanna.tools import (\n    RunSqlTool,\n    VisualizeDataTool,\n    LocalFileSystem,\n)\n\n\nclass VisualizationDemoLlmService(LlmService):\n    \"\"\"Mock LLM that demonstrates SQL query and visualization workflow.\"\"\"\n\n    def __init__(self) -> None:\n        self.step = 0\n        self.csv_filename: Optional[str] = None\n\n    async def send_request(self, request: LlmRequest) -> LlmResponse:\n        \"\"\"Handle non-streaming requests.\"\"\"\n        await asyncio.sleep(0.1)\n        return self._build_response(request)\n\n    async def stream_request(\n        self, request: LlmRequest\n    ) -> AsyncGenerator[LlmStreamChunk, None]:\n        \"\"\"Handle streaming requests.\"\"\"\n        await asyncio.sleep(0.1)\n        response = self._build_response(request)\n\n        if response.tool_calls:\n            yield LlmStreamChunk(tool_calls=response.tool_calls)\n        if response.content:\n            yield LlmStreamChunk(\n                content=response.content, finish_reason=response.finish_reason\n            )\n        else:\n            yield LlmStreamChunk(finish_reason=response.finish_reason)\n\n    async def validate_tools(self, tools: List[ToolSchema]) -> List[str]:\n        \"\"\"Validate tools - no errors.\"\"\"\n        return []\n\n    def _build_response(self, request: LlmRequest) -> LlmResponse:\n        \"\"\"Build response based on conversation state.\"\"\"\n        last_message = request.messages[-1] if request.messages else None\n\n        # If we got a tool result, process it\n        if last_message and last_message.role == \"tool\":\n            tool_result = last_message.content or \"\"\n\n            # Check if this was a SQL query result with a CSV file\n            if \"Results saved to\" in tool_result and \".csv\" in tool_result:\n                # Extract filename from result\n                import re\n\n                match = re.search(r\"'([^']*\\.csv)'\", tool_result)\n                if match:\n                    self.csv_filename = match.group(1)\n                    # Now visualize the data\n                    return LlmResponse(\n                        content=f\"Great! I've saved the query results. Now let me create a visualization of the data.\",\n                        tool_calls=[\n                            ToolCall(\n                                id=f\"call_{uuid.uuid4().hex[:8]}\",\n                                name=\"visualize_data\",\n                                arguments={\"filename\": self.csv_filename},\n                            )\n                        ],\n                        finish_reason=\"tool_calls\",\n                    )\n\n            # If this was a visualization result, acknowledge it\n            if \"Created visualization\" in tool_result:\n                return LlmResponse(\n                    content=f\"Perfect! I've created a visualization of the data. {tool_result}\",\n                    finish_reason=\"stop\",\n                )\n\n            # Default acknowledgment\n            return LlmResponse(\n                content=f\"I've completed the operation. {tool_result}\",\n                finish_reason=\"stop\",\n            )\n\n        # Initial request - run SQL query\n        if self.step == 0:\n            self.step += 1\n            return LlmResponse(\n                content=\"I'll query the database for you and then create a visualization.\",\n                tool_calls=[\n                    ToolCall(\n                        id=f\"call_{uuid.uuid4().hex[:8]}\",\n                        name=\"run_sql\",\n                        arguments={\n                            \"sql\": \"SELECT Name, Milliseconds, Bytes FROM Track LIMIT 20\"\n                        },\n                    )\n                ],\n                finish_reason=\"tool_calls\",\n            )\n\n        # Default response\n        return LlmResponse(\n            content=\"I can help you query databases and visualize the results.\",\n            finish_reason=\"stop\",\n        )\n\n\ndef create_demo_agent() -> Agent:\n    \"\"\"\n    Create a demo agent with SQL and visualization tools.\n\n    This function is called by the vanna server framework.\n\n    Returns:\n        Configured Agent with SQL and visualization tools\n    \"\"\"\n    # Check for Chinook database\n    database_path = os.path.join(\n        os.path.dirname(__file__), \"..\", \"..\", \"Chinook.sqlite\"\n    )\n    database_path = os.path.abspath(database_path)\n\n    if not os.path.exists(database_path):\n        raise FileNotFoundError(\n            f\"Chinook database not found at {database_path}. \"\n            \"Please download it from https://vanna.ai/Chinook.sqlite\"\n        )\n\n    # Create shared FileSystem for both tools\n    file_system = LocalFileSystem(working_directory=\"./data_storage\")\n\n    # Create SQL tool with FileSystem\n    sqlite_runner = SqliteRunner(database_path=database_path)\n    sql_tool = RunSqlTool(sql_runner=sqlite_runner, file_system=file_system)\n\n    # Create visualization tool with same FileSystem\n    viz_tool = VisualizeDataTool(file_system=file_system)\n\n    # Create tool registry\n    tool_registry = ToolRegistry()\n    tool_registry.register(sql_tool)\n    tool_registry.register(viz_tool)\n\n    # Create LLM service\n    llm_service = VisualizationDemoLlmService()\n\n    # Create agent with streaming enabled for web interface\n    return Agent(\n        llm_service=llm_service,\n        tool_registry=tool_registry,\n        config=AgentConfig(\n            stream_responses=True,\n            include_thinking_indicators=False,\n        ),\n    )\n\n\nasync def main() -> None:\n    \"\"\"Demonstrate SQL query execution with automatic visualization.\"\"\"\n    print(\"🎨 SQL + Visualization Demo\")\n    print(\"=\" * 60)\n    print(\"This example demonstrates:\")\n    print(\"1. Running SQL queries that save results to CSV files\")\n    print(\"2. Automatically visualizing the CSV data\")\n    print(\"3. User isolation for file storage\")\n    print(\"=\" * 60)\n    print()\n\n    # Create agent using factory function\n    agent = create_demo_agent()\n\n    # Create test user\n    user = User(id=\"demo-user\", username=\"demo\")\n\n    # Show available tools\n    tools = await agent.get_available_tools(user)\n    print(f\"Available tools: {[tool.name for tool in tools]}\")\n    print()\n\n    # Run conversation\n    conversation_id = \"viz-demo\"\n\n    print(\"User: Show me some track data and visualize it\")\n    print()\n\n    async for component in agent.send_message(\n        user=user,\n        message=\"Show me some track data and visualize it\",\n        conversation_id=conversation_id,\n    ):\n        if (\n            component.simple_component\n            and hasattr(component.simple_component, \"text\")\n            and component.simple_component.text\n        ):\n            print(f\"Agent: {component.simple_component.text}\")\n        elif component.simple_component and hasattr(component.simple_component, \"text\"):\n            print(f\"Agent: {component.simple_component.text}\")\n        elif hasattr(component.rich_component, \"content\"):\n            if isinstance(component.rich_component.content, dict):\n                # This is the chart\n                print(\n                    f\"Agent: [Chart Generated - Plotly figure with {len(str(component.rich_component.content))} chars]\"\n                )\n            else:\n                print(f\"Agent: {component.rich_component.content}\")\n\n    print()\n    print(\"=\" * 60)\n    print(\"Demo complete!\")\n    print()\n    print(\"Key features demonstrated:\")\n    print(\"✅ SQL queries save results to user-isolated CSV files\")\n    print(\"✅ Visualization tool reads CSV files using FileSystem\")\n    print(\"✅ Automatic chart type selection based on data shape\")\n    print(\"✅ Dependency injection allows customization\")\n    print()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "src/vanna/integrations/__init__.py",
    "content": "\"\"\"\nIntegrations module.\n\nThis package contains concrete implementations of core abstractions and capabilities.\n\"\"\"\n\nfrom .local import MemoryConversationStore\nfrom .mock import MockLlmService\nfrom .plotly import PlotlyChartGenerator\nfrom .sqlite import SqliteRunner\n\n__all__ = [\n    \"MockLlmService\",\n    \"MemoryConversationStore\",\n    \"SqliteRunner\",\n    \"PlotlyChartGenerator\",\n]\n"
  },
  {
    "path": "src/vanna/integrations/anthropic/__init__.py",
    "content": "\"\"\"\nAnthropic integration.\n\nThis module provides Anthropic LLM service implementation.\n\"\"\"\n\nfrom .llm import AnthropicLlmService\n\n__all__ = [\"AnthropicLlmService\"]\n"
  },
  {
    "path": "src/vanna/integrations/anthropic/llm.py",
    "content": "\"\"\"\nAnthropic LLM service implementation.\n\nImplements the LlmService interface using Anthropic's Messages API\n(anthropic>=0.8.0). Supports non-streaming and streaming text output.\nTool-calls (tool_use blocks) are surfaced at the end of a stream or after a\nnon-streaming call as ToolCall entries.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport os\nfrom typing import Any, AsyncGenerator, Dict, List, Optional, Tuple\n\nlogger = logging.getLogger(__name__)\n\nfrom vanna.core.llm import (\n    LlmService,\n    LlmRequest,\n    LlmResponse,\n    LlmStreamChunk,\n)\nfrom vanna.core.tool import ToolCall, ToolSchema\n\n\nclass AnthropicLlmService(LlmService):\n    \"\"\"Anthropic Messages-backed LLM service.\n\n    Args:\n        model: Anthropic model name (e.g., \"claude-sonnet-4-5\", \"claude-opus-4\").\n            Defaults to \"claude-sonnet-4-5\". Can also be set via ANTHROPIC_MODEL env var.\n        api_key: API key; falls back to env `ANTHROPIC_API_KEY`.\n        base_url: Optional custom base URL; env `ANTHROPIC_BASE_URL` if unset.\n        extra_client_kwargs: Extra kwargs forwarded to `anthropic.Anthropic()`.\n    \"\"\"\n\n    def __init__(\n        self,\n        model: Optional[str] = None,\n        api_key: Optional[str] = None,\n        base_url: Optional[str] = None,\n        **extra_client_kwargs: Any,\n    ) -> None:\n        try:\n            import anthropic\n        except Exception as e:  # pragma: no cover\n            raise ImportError(\n                \"anthropic package is required. Install with: pip install 'vanna[anthropic]'\"\n            ) from e\n\n        # Model selection - use environment variable or default\n        self.model = model or os.getenv(\"ANTHROPIC_MODEL\", \"claude-sonnet-4-5\")\n        api_key = api_key or os.getenv(\"ANTHROPIC_API_KEY\")\n        base_url = base_url or os.getenv(\"ANTHROPIC_BASE_URL\")\n\n        client_kwargs: Dict[str, Any] = {**extra_client_kwargs}\n        if api_key:\n            client_kwargs[\"api_key\"] = api_key\n        if base_url:\n            client_kwargs[\"base_url\"] = base_url\n\n        self._client = anthropic.Anthropic(**client_kwargs)\n\n    async def send_request(self, request: LlmRequest) -> LlmResponse:\n        \"\"\"Send a non-streaming request to Anthropic and return the response.\"\"\"\n        payload = self._build_payload(request)\n\n        resp = self._client.messages.create(**payload)\n\n        logger.info(f\"Anthropic response: {resp}\")\n\n        text_content, tool_calls = self._parse_message_content(resp)\n\n        usage: Dict[str, int] = {}\n        if getattr(resp, \"usage\", None):\n            try:\n                usage = {\n                    \"input_tokens\": int(resp.usage.input_tokens),\n                    \"output_tokens\": int(resp.usage.output_tokens),\n                }\n            except Exception:\n                pass\n\n        return LlmResponse(\n            content=text_content or None,\n            tool_calls=tool_calls or None,\n            finish_reason=getattr(resp, \"stop_reason\", None),\n            usage=usage or None,\n        )\n\n    async def stream_request(\n        self, request: LlmRequest\n    ) -> AsyncGenerator[LlmStreamChunk, None]:\n        \"\"\"Stream a request to Anthropic.\n\n        Yields text chunks as they arrive. Emits tool-calls at the end by\n        inspecting the final message.\n        \"\"\"\n        payload = self._build_payload(request)\n\n        logger.info(f\"Anthropic streaming payload: {payload}\")\n\n        # SDK provides a streaming context manager with a text_stream iterator.\n        with self._client.messages.stream(**payload) as stream:\n            for text in stream.text_stream:\n                if text:\n                    yield LlmStreamChunk(content=text)\n\n            final = stream.get_final_message()\n            logger.info(f\"Anthropic stream response: {final}\")\n            _, tool_calls = self._parse_message_content(final)\n            if tool_calls:\n                yield LlmStreamChunk(\n                    tool_calls=tool_calls,\n                    finish_reason=getattr(final, \"stop_reason\", None),\n                )\n            else:\n                yield LlmStreamChunk(\n                    finish_reason=getattr(final, \"stop_reason\", None) or \"stop\"\n                )\n\n    async def validate_tools(self, tools: List[ToolSchema]) -> List[str]:\n        \"\"\"Basic validation of tool schemas for Anthropic.\"\"\"\n        errors: List[str] = []\n        for t in tools:\n            if not t.name:\n                errors.append(\"Tool name is required\")\n        return errors\n\n    # Internal helpers\n    def _build_payload(self, request: LlmRequest) -> Dict[str, Any]:\n        # Anthropic requires messages content as list of content blocks per message\n        # We need to group consecutive tool messages into single user messages\n        messages: List[Dict[str, Any]] = []\n        i = 0\n\n        while i < len(request.messages):\n            m = request.messages[i]\n\n            if m.role == \"tool\":\n                # Group consecutive tool messages into one user message\n                tool_content_blocks = []\n                while i < len(request.messages) and request.messages[i].role == \"tool\":\n                    tool_msg = request.messages[i]\n                    if tool_msg.tool_call_id:\n                        tool_content_blocks.append(\n                            {\n                                \"type\": \"tool_result\",\n                                \"tool_use_id\": tool_msg.tool_call_id,\n                                \"content\": tool_msg.content,\n                            }\n                        )\n                    i += 1\n\n                if tool_content_blocks:\n                    messages.append(\n                        {\n                            \"role\": \"user\",\n                            \"content\": tool_content_blocks,\n                        }\n                    )\n            else:\n                # Handle non-tool messages normally\n                content_blocks = []\n\n                # Handle text content - only add if not empty\n                if m.content and m.content.strip():\n                    content_blocks.append({\"type\": \"text\", \"text\": m.content})\n\n                # Handle tool_calls for assistant messages (convert to tool_use blocks)\n                if m.role == \"assistant\" and m.tool_calls:\n                    for tc in m.tool_calls:\n                        content_blocks.append(\n                            {\n                                \"type\": \"tool_use\",\n                                \"id\": tc.id,\n                                \"name\": tc.name,\n                                \"input\": tc.arguments,  # type: ignore[dict-item]\n                            }\n                        )\n\n                # Ensure we have at least one content block for text messages\n                if not content_blocks and m.role in {\"user\", \"assistant\"}:\n                    content_blocks.append({\"type\": \"text\", \"text\": m.content or \"\"})\n\n                if content_blocks:\n                    role = m.role if m.role in {\"user\", \"assistant\"} else \"user\"\n                    messages.append(\n                        {\n                            \"role\": role,\n                            \"content\": content_blocks,\n                        }\n                    )\n\n                i += 1\n\n        tools_payload: Optional[List[Dict[str, Any]]] = None\n        if request.tools:\n            tools_payload = [\n                {\n                    \"name\": t.name,\n                    \"description\": t.description,\n                    \"input_schema\": t.parameters,\n                }\n                for t in request.tools\n            ]\n\n        payload: Dict[str, Any] = {\n            \"model\": self.model,\n            \"messages\": messages,\n            # Anthropic requires max_tokens; default if not provided\n            \"max_tokens\": request.max_tokens if request.max_tokens is not None else 512,\n            \"temperature\": request.temperature,\n        }\n        if tools_payload:\n            payload[\"tools\"] = tools_payload\n            payload[\"tool_choice\"] = {\"type\": \"auto\"}\n\n        # Add system prompt if provided\n        if request.system_prompt:\n            payload[\"system\"] = request.system_prompt\n\n        return payload\n\n    def _parse_message_content(self, msg: Any) -> Tuple[str, List[ToolCall]]:\n        text_parts: List[str] = []\n        tool_calls: List[ToolCall] = []\n\n        content_list = getattr(msg, \"content\", []) or []\n        for block in content_list:\n            btype = getattr(block, \"type\", None) or (\n                block.get(\"type\") if isinstance(block, dict) else None\n            )\n            if btype == \"text\":\n                # SDK returns block.text for typed object; dict uses {\"text\": ...}\n                text = getattr(block, \"text\", None)\n                if text is None and isinstance(block, dict):\n                    text = block.get(\"text\")\n                if text:\n                    text_parts.append(str(text))\n            elif btype == \"tool_use\":\n                # Tool call with name and input\n                name = getattr(block, \"name\", None) or (\n                    block.get(\"name\") if isinstance(block, dict) else None\n                )\n                tc_id = getattr(block, \"id\", None) or (\n                    block.get(\"id\") if isinstance(block, dict) else None\n                )\n                input_data = getattr(block, \"input\", None) or (\n                    block.get(\"input\") if isinstance(block, dict) else None\n                )\n                if name:\n                    try:\n                        # input_data should be a dict already\n                        args = (\n                            input_data\n                            if isinstance(input_data, dict)\n                            else {\"_raw\": input_data}\n                        )\n                    except Exception:\n                        args = {\"_raw\": str(input_data)}\n                    tool_calls.append(\n                        ToolCall(\n                            id=str(tc_id or \"tool_call\"), name=str(name), arguments=args\n                        )\n                    )\n\n        text_content = \"\".join(text_parts)\n        return text_content, tool_calls\n"
  },
  {
    "path": "src/vanna/integrations/azureopenai/__init__.py",
    "content": "\"\"\"\nAzure OpenAI integration.\n\nThis module provides Azure OpenAI LLM service implementations.\n\"\"\"\n\nfrom .llm import AzureOpenAILlmService\n\n__all__ = [\"AzureOpenAILlmService\"]\n"
  },
  {
    "path": "src/vanna/integrations/azureopenai/llm.py",
    "content": "\"\"\"\nAzure OpenAI LLM service implementation.\n\nProvides an `LlmService` backed by Azure OpenAI Chat Completions (openai>=1.0.0)\nwith support for streaming, deployment-scoped models, and Azure-specific\nauthentication flows.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nfrom typing import Any, AsyncGenerator, Dict, List, Optional, Set\n\nfrom vanna.core.llm import (\n    LlmService,\n    LlmRequest,\n    LlmResponse,\n    LlmStreamChunk,\n)\nfrom vanna.core.tool import ToolCall, ToolSchema\n\n\n# Models that don't support temperature and other sampling parameters\nREASONING_MODELS: Set[str] = {\n    \"o1\",\n    \"o1-mini\",\n    \"o1-preview\",\n    \"o3-mini\",\n    \"gpt-5\",\n    \"gpt-5-mini\",\n    \"gpt-5-nano\",\n    \"gpt-5-pro\",\n    \"gpt-5-codex\",\n}\n\n\ndef _is_reasoning_model(model: str) -> bool:\n    \"\"\"Return True when the deployment targets a reasoning-only model.\"\"\"\n    model_lower = model.lower()\n    return any(reasoning_model in model_lower for reasoning_model in REASONING_MODELS)\n\n\nclass AzureOpenAILlmService(LlmService):\n    \"\"\"Azure OpenAI Chat Completions-backed LLM service.\n\n    Wraps `openai.AzureOpenAI` so Vanna can talk to deployment-scoped models\n    and either API key or Microsoft Entra ID authentication.\n\n    Args:\n        model: Deployment name in Azure OpenAI (required).\n        api_key: API key; falls back to `AZURE_OPENAI_API_KEY`.\n        azure_endpoint: Azure OpenAI endpoint URL; falls back to\n            `AZURE_OPENAI_ENDPOINT`.\n        api_version: API version; defaults to \"2024-10-21\" or\n            `AZURE_OPENAI_API_VERSION`.\n        azure_ad_token_provider: Optional bearer token provider for Entra ID.\n        **extra_client_kwargs: Additional keyword arguments forwarded to the\n            underlying client.\n    \"\"\"\n\n    def __init__(\n        self,\n        model: Optional[str] = None,\n        api_key: Optional[str] = None,\n        azure_endpoint: Optional[str] = None,\n        api_version: Optional[str] = None,\n        azure_ad_token_provider: Optional[Any] = None,\n        **extra_client_kwargs: Any,\n    ) -> None:\n        try:\n            from openai import AzureOpenAI\n        except Exception as e:  # pragma: no cover\n            raise ImportError(\n                \"openai package is required. Install with: pip install 'vanna[azureopenai]' \"\n                \"or 'pip install openai'\"\n            ) from e\n\n        # Model/deployment name is required for Azure OpenAI\n        self.model = model or os.getenv(\"AZURE_OPENAI_MODEL\")\n        if not self.model:\n            raise ValueError(\n                \"model parameter (deployment name) is required for Azure OpenAI. \"\n                \"Provide it as argument or set AZURE_OPENAI_MODEL environment variable.\"\n            )\n\n        # Azure endpoint is required\n        azure_endpoint = azure_endpoint or os.getenv(\"AZURE_OPENAI_ENDPOINT\")\n        if not azure_endpoint:\n            raise ValueError(\n                \"azure_endpoint is required for Azure OpenAI. \"\n                \"Provide it as argument or set AZURE_OPENAI_ENDPOINT environment variable.\"\n            )\n\n        # API version - use latest stable GA version by default\n        api_version = api_version or os.getenv(\"AZURE_OPENAI_API_VERSION\", \"2024-10-21\")\n\n        # Build client kwargs\n        client_kwargs: Dict[str, Any] = {\n            \"azure_endpoint\": azure_endpoint,\n            \"api_version\": api_version,\n            **extra_client_kwargs,\n        }\n\n        # Authentication: prefer Azure AD token provider, fallback to API key\n        if azure_ad_token_provider is not None:\n            client_kwargs[\"azure_ad_token_provider\"] = azure_ad_token_provider\n        else:\n            api_key = api_key or os.getenv(\"AZURE_OPENAI_API_KEY\")\n            if not api_key:\n                raise ValueError(\n                    \"Authentication required: provide either api_key or azure_ad_token_provider. \"\n                    \"API key can also be set via AZURE_OPENAI_API_KEY environment variable.\"\n                )\n            client_kwargs[\"api_key\"] = api_key\n\n        self._client = AzureOpenAI(**client_kwargs)\n        self._is_reasoning_model = _is_reasoning_model(self.model)\n\n    async def send_request(self, request: LlmRequest) -> LlmResponse:\n        \"\"\"Send a non-streaming request to Azure OpenAI and return the response.\"\"\"\n        payload = self._build_payload(request)\n\n        # Call the API synchronously; this function is async but we can block here.\n        resp = self._client.chat.completions.create(**payload, stream=False)\n\n        if not resp.choices:\n            return LlmResponse(content=None, tool_calls=None, finish_reason=None)\n\n        choice = resp.choices[0]\n        content: Optional[str] = getattr(choice.message, \"content\", None)\n        tool_calls = self._extract_tool_calls_from_message(choice.message)\n\n        usage: Dict[str, int] = {}\n        if getattr(resp, \"usage\", None):\n            usage = {\n                k: int(v)\n                for k, v in {\n                    \"prompt_tokens\": getattr(resp.usage, \"prompt_tokens\", 0),\n                    \"completion_tokens\": getattr(resp.usage, \"completion_tokens\", 0),\n                    \"total_tokens\": getattr(resp.usage, \"total_tokens\", 0),\n                }.items()\n            }\n\n        return LlmResponse(\n            content=content,\n            tool_calls=tool_calls or None,\n            finish_reason=getattr(choice, \"finish_reason\", None),\n            usage=usage or None,\n        )\n\n    async def stream_request(\n        self, request: LlmRequest\n    ) -> AsyncGenerator[LlmStreamChunk, None]:\n        \"\"\"\n        Stream a request to Azure OpenAI.\n\n        Emits `LlmStreamChunk` for textual deltas as they arrive. Tool-calls are\n        accumulated and emitted in a final chunk when the stream ends.\n        \"\"\"\n        payload = self._build_payload(request)\n\n        # Synchronous streaming iterator; iterate within async context.\n        stream = self._client.chat.completions.create(**payload, stream=True)\n\n        # Builders for streamed tool-calls (index -> partial)\n        tc_builders: Dict[int, Dict[str, Optional[str]]] = {}\n        last_finish: Optional[str] = None\n\n        for event in stream:\n            if not getattr(event, \"choices\", None):\n                continue\n\n            choice = event.choices[0]\n            delta = getattr(choice, \"delta\", None)\n            if delta is None:\n                # Some SDK versions use `event.choices[0].message` on the final packet\n                last_finish = getattr(choice, \"finish_reason\", last_finish)\n                continue\n\n            # Text content\n            content_piece: Optional[str] = getattr(delta, \"content\", None)\n            if content_piece:\n                yield LlmStreamChunk(content=content_piece)\n\n            # Tool calls (streamed)\n            streamed_tool_calls = getattr(delta, \"tool_calls\", None)\n            if streamed_tool_calls:\n                for tc in streamed_tool_calls:\n                    idx = getattr(tc, \"index\", 0) or 0\n                    b = tc_builders.setdefault(\n                        idx, {\"id\": None, \"name\": None, \"arguments\": \"\"}\n                    )\n                    if getattr(tc, \"id\", None):\n                        b[\"id\"] = tc.id\n                    fn = getattr(tc, \"function\", None)\n                    if fn is not None:\n                        if getattr(fn, \"name\", None):\n                            b[\"name\"] = fn.name\n                        if getattr(fn, \"arguments\", None):\n                            b[\"arguments\"] = (b[\"arguments\"] or \"\") + fn.arguments\n\n            last_finish = getattr(choice, \"finish_reason\", last_finish)\n\n        # Emit final tool-calls chunk if any\n        final_tool_calls: List[ToolCall] = []\n        for b in tc_builders.values():\n            if not b.get(\"name\"):\n                continue\n            args_raw = b.get(\"arguments\") or \"{}\"\n            try:\n                loaded = json.loads(args_raw)\n                if isinstance(loaded, dict):\n                    args_dict: Dict[str, Any] = loaded\n                else:\n                    args_dict = {\"args\": loaded}\n            except Exception:\n                args_dict = {\"_raw\": args_raw}\n            final_tool_calls.append(\n                ToolCall(\n                    id=b.get(\"id\") or \"tool_call\",\n                    name=b[\"name\"] or \"tool\",\n                    arguments=args_dict,\n                )\n            )\n\n        if final_tool_calls:\n            yield LlmStreamChunk(tool_calls=final_tool_calls, finish_reason=last_finish)\n        else:\n            # Still emit a terminal chunk to signal completion\n            yield LlmStreamChunk(finish_reason=last_finish or \"stop\")\n\n    async def validate_tools(self, tools: List[ToolSchema]) -> List[str]:\n        \"\"\"Validate tool schemas. Returns a list of error messages.\"\"\"\n        errors: List[str] = []\n        # Basic checks; Azure OpenAI will enforce further validation server-side.\n        for t in tools:\n            if not t.name or len(t.name) > 64:\n                errors.append(f\"Invalid tool name: {t.name!r}\")\n        return errors\n\n    # Internal helpers\n    def _build_payload(self, request: LlmRequest) -> Dict[str, Any]:\n        \"\"\"Build the API payload from LlmRequest.\"\"\"\n        messages: List[Dict[str, Any]] = []\n\n        # Add system prompt as first message if provided\n        if request.system_prompt:\n            messages.append({\"role\": \"system\", \"content\": request.system_prompt})\n\n        for m in request.messages:\n            msg: Dict[str, Any] = {\"role\": m.role, \"content\": m.content}\n            if m.role == \"tool\" and m.tool_call_id:\n                msg[\"tool_call_id\"] = m.tool_call_id\n            elif m.role == \"assistant\" and m.tool_calls:\n                # Convert tool calls to OpenAI format\n                tool_calls_payload = []\n                for tc in m.tool_calls:\n                    tool_calls_payload.append(\n                        {\n                            \"id\": tc.id,\n                            \"type\": \"function\",\n                            \"function\": {\n                                \"name\": tc.name,\n                                \"arguments\": json.dumps(tc.arguments),\n                            },\n                        }\n                    )\n                msg[\"tool_calls\"] = tool_calls_payload\n            messages.append(msg)\n\n        tools_payload: Optional[List[Dict[str, Any]]] = None\n        if request.tools:\n            tools_payload = [\n                {\n                    \"type\": \"function\",\n                    \"function\": {\n                        \"name\": t.name,\n                        \"description\": t.description,\n                        \"parameters\": t.parameters,\n                    },\n                }\n                for t in request.tools\n            ]\n\n        payload: Dict[str, Any] = {\n            \"model\": self.model,\n            \"messages\": messages,\n        }\n\n        # Add temperature only for non-reasoning models\n        # Reasoning models (GPT-5, o1, o3-mini) don't support temperature parameter\n        if not self._is_reasoning_model:\n            payload[\"temperature\"] = request.temperature\n\n        if request.max_tokens is not None:\n            payload[\"max_tokens\"] = request.max_tokens\n\n        if tools_payload:\n            payload[\"tools\"] = tools_payload\n            payload[\"tool_choice\"] = \"auto\"\n\n        return payload\n\n    def _extract_tool_calls_from_message(self, message: Any) -> List[ToolCall]:\n        \"\"\"Extract tool calls from OpenAI message object.\"\"\"\n        tool_calls: List[ToolCall] = []\n        raw_tool_calls = getattr(message, \"tool_calls\", None) or []\n        for tc in raw_tool_calls:\n            fn = getattr(tc, \"function\", None)\n            if not fn:\n                continue\n            args_raw = getattr(fn, \"arguments\", \"{}\")\n            try:\n                loaded = json.loads(args_raw)\n                if isinstance(loaded, dict):\n                    args_dict: Dict[str, Any] = loaded\n                else:\n                    args_dict = {\"args\": loaded}\n            except Exception:\n                args_dict = {\"_raw\": args_raw}\n            tool_calls.append(\n                ToolCall(\n                    id=getattr(tc, \"id\", \"tool_call\"),\n                    name=getattr(fn, \"name\", \"tool\"),\n                    arguments=args_dict,\n                )\n            )\n        return tool_calls\n"
  },
  {
    "path": "src/vanna/integrations/azuresearch/__init__.py",
    "content": "\"\"\"\nAzure AI Search integration for Vanna Agents.\n\"\"\"\n\nfrom .agent_memory import AzureAISearchAgentMemory\n\n__all__ = [\"AzureAISearchAgentMemory\"]\n"
  },
  {
    "path": "src/vanna/integrations/azuresearch/agent_memory.py",
    "content": "\"\"\"\nAzure AI Search implementation of AgentMemory.\n\nThis implementation uses Azure Cognitive Search for vector storage of tool usage patterns.\n\"\"\"\n\nimport json\nimport uuid\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional\nimport asyncio\nfrom concurrent.futures import ThreadPoolExecutor\n\ntry:\n    from azure.search.documents import SearchClient\n    from azure.search.documents.indexes import SearchIndexClient\n    from azure.search.documents.indexes.models import (\n        SearchIndex,\n        SearchField,\n        SearchFieldDataType,\n        VectorSearch,\n        VectorSearchAlgorithmConfiguration,\n    )\n    from azure.core.credentials import AzureKeyCredential\n\n    AZURE_SEARCH_AVAILABLE = True\nexcept ImportError:\n    AZURE_SEARCH_AVAILABLE = False\n\nfrom vanna.capabilities.agent_memory import (\n    AgentMemory,\n    TextMemory,\n    TextMemorySearchResult,\n    ToolMemory,\n    ToolMemorySearchResult,\n)\nfrom vanna.core.tool import ToolContext\n\n\nclass AzureAISearchAgentMemory(AgentMemory):\n    \"\"\"Azure AI Search-based implementation of AgentMemory.\"\"\"\n\n    def __init__(\n        self,\n        endpoint: str,\n        api_key: str,\n        index_name: str = \"tool-memories\",\n        dimension: int = 384,\n    ):\n        if not AZURE_SEARCH_AVAILABLE:\n            raise ImportError(\n                \"Azure Search is required for AzureAISearchAgentMemory. \"\n                \"Install with: pip install azure-search-documents\"\n            )\n\n        self.endpoint = endpoint\n        self.api_key = api_key\n        self.index_name = index_name\n        self.dimension = dimension\n        self._credential = AzureKeyCredential(api_key)\n        self._search_client = None\n        self._index_client = None\n        self._executor = ThreadPoolExecutor(max_workers=2)\n\n    def _get_index_client(self):\n        \"\"\"Get or create index client.\"\"\"\n        if self._index_client is None:\n            self._index_client = SearchIndexClient(\n                endpoint=self.endpoint, credential=self._credential\n            )\n            self._ensure_index_exists()\n        return self._index_client\n\n    def _get_search_client(self):\n        \"\"\"Get or create search client.\"\"\"\n        if self._search_client is None:\n            self._get_index_client()  # Ensure index exists\n            self._search_client = SearchClient(\n                endpoint=self.endpoint,\n                index_name=self.index_name,\n                credential=self._credential,\n            )\n        return self._search_client\n\n    def _ensure_index_exists(self):\n        \"\"\"Create index if it doesn't exist.\"\"\"\n        try:\n            self._index_client.get_index(self.index_name)\n        except Exception:\n            # Create index with vector search configuration\n            fields = [\n                SearchField(\n                    name=\"memory_id\", type=SearchFieldDataType.String, key=True\n                ),\n                SearchField(\n                    name=\"question\", type=SearchFieldDataType.String, searchable=True\n                ),\n                SearchField(\n                    name=\"tool_name\", type=SearchFieldDataType.String, filterable=True\n                ),\n                SearchField(name=\"args_json\", type=SearchFieldDataType.String),\n                SearchField(\n                    name=\"timestamp\",\n                    type=SearchFieldDataType.String,\n                    sortable=True,\n                    filterable=True,\n                ),\n                SearchField(\n                    name=\"success\", type=SearchFieldDataType.Boolean, filterable=True\n                ),\n                SearchField(name=\"metadata_json\", type=SearchFieldDataType.String),\n                SearchField(\n                    name=\"embedding\",\n                    type=SearchFieldDataType.Collection(SearchFieldDataType.Single),\n                    searchable=True,\n                    vector_search_dimensions=self.dimension,\n                    vector_search_configuration=\"vector-config\",\n                ),\n            ]\n\n            vector_search = VectorSearch(\n                algorithm_configurations=[\n                    VectorSearchAlgorithmConfiguration(name=\"vector-config\")\n                ]\n            )\n\n            index = SearchIndex(\n                name=self.index_name, fields=fields, vector_search=vector_search\n            )\n\n            self._index_client.create_index(index)\n\n    def _create_embedding(self, text: str) -> List[float]:\n        \"\"\"Create a simple embedding from text (placeholder).\"\"\"\n        import hashlib\n\n        hash_val = int(hashlib.md5(text.encode()).hexdigest(), 16)\n        return [(hash_val >> i) % 100 / 100.0 for i in range(self.dimension)]\n\n    async def save_tool_usage(\n        self,\n        question: str,\n        tool_name: str,\n        args: Dict[str, Any],\n        context: ToolContext,\n        success: bool = True,\n        metadata: Optional[Dict[str, Any]] = None,\n    ) -> None:\n        \"\"\"Save a tool usage pattern.\"\"\"\n\n        def _save():\n            client = self._get_search_client()\n\n            memory_id = str(uuid.uuid4())\n            timestamp = datetime.now().isoformat()\n            embedding = self._create_embedding(question)\n\n            document = {\n                \"memory_id\": memory_id,\n                \"question\": question,\n                \"tool_name\": tool_name,\n                \"args_json\": json.dumps(args),\n                \"timestamp\": timestamp,\n                \"success\": success,\n                \"metadata_json\": json.dumps(metadata or {}),\n                \"embedding\": embedding,\n            }\n\n            client.upload_documents(documents=[document])\n\n        await asyncio.get_event_loop().run_in_executor(self._executor, _save)\n\n    async def search_similar_usage(\n        self,\n        question: str,\n        context: ToolContext,\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n        tool_name_filter: Optional[str] = None,\n    ) -> List[ToolMemorySearchResult]:\n        \"\"\"Search for similar tool usage patterns.\"\"\"\n\n        def _search():\n            client = self._get_search_client()\n\n            embedding = self._create_embedding(question)\n\n            # Build filter\n            filter_expr = \"success eq true\"\n            if tool_name_filter:\n                filter_expr += f\" and tool_name eq '{tool_name_filter}'\"\n\n            results = client.search(\n                search_text=None, vector=embedding, top_k=limit, filter=filter_expr\n            )\n\n            search_results = []\n            for i, doc in enumerate(results):\n                # Azure returns similarity score in @search.score\n                similarity_score = doc.get(\"@search.score\", 0)\n\n                if similarity_score >= similarity_threshold:\n                    args = json.loads(doc.get(\"args_json\", \"{}\"))\n                    metadata_dict = json.loads(doc.get(\"metadata_json\", \"{}\"))\n\n                    memory = ToolMemory(\n                        memory_id=doc[\"memory_id\"],\n                        question=doc[\"question\"],\n                        tool_name=doc[\"tool_name\"],\n                        args=args,\n                        timestamp=doc.get(\"timestamp\"),\n                        success=doc.get(\"success\", True),\n                        metadata=metadata_dict,\n                    )\n\n                    search_results.append(\n                        ToolMemorySearchResult(\n                            memory=memory, similarity_score=similarity_score, rank=i + 1\n                        )\n                    )\n\n            return search_results\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _search)\n\n    async def get_recent_memories(\n        self, context: ToolContext, limit: int = 10\n    ) -> List[ToolMemory]:\n        \"\"\"Get recently added memories.\"\"\"\n\n        def _get_recent():\n            client = self._get_search_client()\n\n            results = client.search(\n                search_text=\"*\", top=limit, order_by=[\"timestamp desc\"]\n            )\n\n            memories = []\n            for doc in results:\n                args = json.loads(doc.get(\"args_json\", \"{}\"))\n                metadata_dict = json.loads(doc.get(\"metadata_json\", \"{}\"))\n\n                memory = ToolMemory(\n                    memory_id=doc[\"memory_id\"],\n                    question=doc[\"question\"],\n                    tool_name=doc[\"tool_name\"],\n                    args=args,\n                    timestamp=doc.get(\"timestamp\"),\n                    success=doc.get(\"success\", True),\n                    metadata=metadata_dict,\n                )\n                memories.append(memory)\n\n            return memories\n\n        return await asyncio.get_event_loop().run_in_executor(\n            self._executor, _get_recent\n        )\n\n    async def delete_by_id(self, context: ToolContext, memory_id: str) -> bool:\n        \"\"\"Delete a memory by its ID.\"\"\"\n\n        def _delete():\n            client = self._get_search_client()\n\n            try:\n                client.delete_documents(documents=[{\"memory_id\": memory_id}])\n                return True\n            except Exception:\n                return False\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _delete)\n\n    async def save_text_memory(self, content: str, context: ToolContext) -> TextMemory:\n        \"\"\"Save a text memory.\"\"\"\n\n        def _save():\n            client = self._get_search_client()\n\n            memory_id = str(uuid.uuid4())\n            timestamp = datetime.now().isoformat()\n            embedding = self._create_embedding(content)\n\n            document = {\n                \"memory_id\": memory_id,\n                \"content\": content,\n                \"timestamp\": timestamp,\n                \"embedding\": embedding,\n            }\n\n            client.upload_documents(documents=[document])\n\n            return TextMemory(memory_id=memory_id, content=content, timestamp=timestamp)\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _save)\n\n    async def search_text_memories(\n        self,\n        query: str,\n        context: ToolContext,\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n    ) -> List[TextMemorySearchResult]:\n        \"\"\"Search for similar text memories.\"\"\"\n\n        def _search():\n            client = self._get_search_client()\n\n            embedding = self._create_embedding(query)\n\n            results = client.search(search_text=None, vector=embedding, top_k=limit)\n\n            search_results = []\n            for i, doc in enumerate(results):\n                similarity_score = doc.get(\"@search.score\", 0)\n\n                if similarity_score >= similarity_threshold:\n                    memory = TextMemory(\n                        memory_id=doc[\"memory_id\"],\n                        content=doc.get(\"content\", \"\"),\n                        timestamp=doc.get(\"timestamp\"),\n                    )\n\n                    search_results.append(\n                        TextMemorySearchResult(\n                            memory=memory, similarity_score=similarity_score, rank=i + 1\n                        )\n                    )\n\n            return search_results\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _search)\n\n    async def get_recent_text_memories(\n        self, context: ToolContext, limit: int = 10\n    ) -> List[TextMemory]:\n        \"\"\"Get recently added text memories.\"\"\"\n\n        def _get_recent():\n            client = self._get_search_client()\n\n            results = client.search(\n                search_text=\"*\", top=limit, order_by=[\"timestamp desc\"]\n            )\n\n            memories = []\n            for doc in results:\n                # Skip if this is a tool memory (has tool_name field)\n                if \"tool_name\" in doc:\n                    continue\n\n                memory = TextMemory(\n                    memory_id=doc[\"memory_id\"],\n                    content=doc.get(\"content\", \"\"),\n                    timestamp=doc.get(\"timestamp\"),\n                )\n                memories.append(memory)\n\n            return memories[:limit]\n\n        return await asyncio.get_event_loop().run_in_executor(\n            self._executor, _get_recent\n        )\n\n    async def delete_text_memory(self, context: ToolContext, memory_id: str) -> bool:\n        \"\"\"Delete a text memory by its ID.\"\"\"\n\n        def _delete():\n            client = self._get_search_client()\n\n            try:\n                client.delete_documents(documents=[{\"memory_id\": memory_id}])\n                return True\n            except Exception:\n                return False\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _delete)\n\n    async def clear_memories(\n        self,\n        context: ToolContext,\n        tool_name: Optional[str] = None,\n        before_date: Optional[str] = None,\n    ) -> int:\n        \"\"\"Clear stored memories.\"\"\"\n\n        def _clear():\n            client = self._get_search_client()\n\n            # Build filter\n            filter_parts = []\n            if tool_name:\n                filter_parts.append(f\"tool_name eq '{tool_name}'\")\n            if before_date:\n                filter_parts.append(f\"timestamp lt '{before_date}'\")\n\n            filter_expr = \" and \".join(filter_parts) if filter_parts else None\n\n            # Search for documents to delete\n            results = client.search(\n                search_text=\"*\", filter=filter_expr, select=[\"memory_id\"]\n            )\n\n            docs_to_delete = [{\"memory_id\": doc[\"memory_id\"]} for doc in results]\n\n            if docs_to_delete:\n                client.delete_documents(documents=docs_to_delete)\n\n            return len(docs_to_delete)\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _clear)\n"
  },
  {
    "path": "src/vanna/integrations/bigquery/__init__.py",
    "content": "\"\"\"BigQuery integration for Vanna.\"\"\"\n\nfrom .sql_runner import BigQueryRunner\n\n__all__ = [\"BigQueryRunner\"]\n"
  },
  {
    "path": "src/vanna/integrations/bigquery/sql_runner.py",
    "content": "\"\"\"BigQuery implementation of SqlRunner interface.\"\"\"\n\nfrom typing import Optional\nimport pandas as pd\n\nfrom vanna.capabilities.sql_runner import SqlRunner, RunSqlToolArgs\nfrom vanna.core.tool import ToolContext\n\n\nclass BigQueryRunner(SqlRunner):\n    \"\"\"BigQuery implementation of the SqlRunner interface.\"\"\"\n\n    def __init__(self, project_id: str, cred_file_path: Optional[str] = None, **kwargs):\n        \"\"\"Initialize with BigQuery connection parameters.\n\n        Args:\n            project_id: Google Cloud Project ID\n            cred_file_path: Path to Google Cloud credentials JSON file (optional)\n            **kwargs: Additional google.cloud.bigquery.Client parameters\n        \"\"\"\n        try:\n            from google.cloud import bigquery\n            from google.oauth2 import service_account\n\n            self.bigquery = bigquery\n            self.service_account = service_account\n        except ImportError as e:\n            raise ImportError(\n                \"google-cloud-bigquery package is required. \"\n                \"Install with: pip install 'vanna[bigquery]'\"\n            ) from e\n\n        self.project_id = project_id\n        self.cred_file_path = cred_file_path\n        self.kwargs = kwargs\n        self._client = None\n\n    def _get_client(self):\n        \"\"\"Get or create BigQuery client.\"\"\"\n        if self._client is not None:\n            return self._client\n\n        if self.cred_file_path:\n            import json\n\n            with open(self.cred_file_path, \"r\") as f:\n                credentials = (\n                    self.service_account.Credentials.from_service_account_info(\n                        json.loads(f.read()),\n                        scopes=[\"https://www.googleapis.com/auth/cloud-platform\"],\n                    )\n                )\n            self._client = self.bigquery.Client(\n                project=self.project_id, credentials=credentials, **self.kwargs\n            )\n        else:\n            # Use default credentials\n            self._client = self.bigquery.Client(project=self.project_id, **self.kwargs)\n\n        return self._client\n\n    async def run_sql(self, args: RunSqlToolArgs, context: ToolContext) -> pd.DataFrame:\n        \"\"\"Execute SQL query against BigQuery database and return results as DataFrame.\n\n        Args:\n            args: SQL query arguments\n            context: Tool execution context\n\n        Returns:\n            DataFrame with query results\n\n        Raises:\n            google.api_core.exceptions.GoogleAPIError: If query execution fails\n        \"\"\"\n        client = self._get_client()\n\n        # Execute the query\n        job = client.query(args.sql)\n        df = job.result().to_dataframe()\n\n        return df\n"
  },
  {
    "path": "src/vanna/integrations/chromadb/__init__.py",
    "content": "\"\"\"\nChromaDB integration for Vanna Agents.\n\"\"\"\n\nfrom .agent_memory import ChromaAgentMemory\n\n\ndef get_device() -> str:\n    \"\"\"Detect the best available device for embeddings.\n\n    This function checks for GPU availability and returns the appropriate device string\n    for use with embedding models. It prioritizes hardware acceleration when available.\n\n    Returns:\n        str: Device string - 'cuda' if NVIDIA GPU available, 'mps' if Apple Silicon,\n             'cpu' otherwise.\n\n    Examples:\n        >>> device = get_device()\n        >>> print(f\"Using device: {device}\")\n        Using device: cuda\n\n        # Use with ChromaDB SentenceTransformer embeddings\n        >>> from chromadb.utils import embedding_functions\n        >>> ef = embedding_functions.SentenceTransformerEmbeddingFunction(\n        ...     model_name=\"sentence-transformers/all-MiniLM-L6-v2\",\n        ...     device=get_device()\n        ... )\n        >>> memory = ChromaAgentMemory(embedding_function=ef)\n    \"\"\"\n    try:\n        import torch\n\n        # Check for CUDA (NVIDIA GPUs)\n        if torch.cuda.is_available():\n            return \"cuda\"\n\n        # Check for MPS (Apple Silicon GPUs)\n        if hasattr(torch.backends, \"mps\") and torch.backends.mps.is_available():\n            return \"mps\"\n\n    except ImportError:\n        # PyTorch not installed, fall back to CPU\n        pass\n\n    return \"cpu\"\n\n\ndef create_sentence_transformer_embedding_function(\n    model_name: str = \"sentence-transformers/all-MiniLM-L6-v2\", device: str = None\n):\n    \"\"\"Create a SentenceTransformer embedding function with automatic device detection.\n\n    This convenience function creates a ChromaDB-compatible SentenceTransformer embedding\n    function with intelligent device selection. If no device is specified, it automatically\n    detects and uses the best available hardware (CUDA, MPS, or CPU).\n\n    Note: This requires the 'sentence-transformers' package to be installed.\n    Install with: pip install sentence-transformers\n\n    Args:\n        model_name: The name of the sentence-transformer model to use.\n                   Defaults to \"sentence-transformers/all-MiniLM-L6-v2\".\n        device: Optional device string ('cuda', 'mps', or 'cpu'). If None,\n               automatically detects the best available device.\n\n    Returns:\n        A ChromaDB SentenceTransformer embedding function configured for the\n        specified/detected device.\n\n    Examples:\n        # Automatic device detection (uses CUDA/MPS if available)\n        >>> from vanna.integrations.chromadb import ChromaAgentMemory, create_sentence_transformer_embedding_function\n        >>> ef = create_sentence_transformer_embedding_function()\n        >>> memory = ChromaAgentMemory(embedding_function=ef)\n\n        # Explicitly use CUDA\n        >>> ef_cuda = create_sentence_transformer_embedding_function(device=\"cuda\")\n        >>> memory = ChromaAgentMemory(embedding_function=ef_cuda)\n\n        # Use a different model\n        >>> ef_large = create_sentence_transformer_embedding_function(\n        ...     model_name=\"sentence-transformers/all-mpnet-base-v2\"\n        ... )\n        >>> memory = ChromaAgentMemory(embedding_function=ef_large)\n    \"\"\"\n    try:\n        from chromadb.utils import embedding_functions\n    except ImportError:\n        raise ImportError(\"ChromaDB is required. Install with: pip install chromadb\")\n\n    if device is None:\n        device = get_device()\n\n    return embedding_functions.SentenceTransformerEmbeddingFunction(\n        model_name=model_name, device=device\n    )\n\n\n__all__ = [\n    \"ChromaAgentMemory\",\n    \"get_device\",\n    \"create_sentence_transformer_embedding_function\",\n]\n"
  },
  {
    "path": "src/vanna/integrations/chromadb/agent_memory.py",
    "content": "\"\"\"\nLocal vector database implementation of AgentMemory.\n\nThis implementation uses ChromaDB for local vector storage of tool usage patterns.\n\"\"\"\n\nimport json\nimport hashlib\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional\nimport asyncio\nfrom concurrent.futures import ThreadPoolExecutor\n\ntry:\n    import chromadb\n    from chromadb.config import Settings\n    from chromadb.utils import embedding_functions\n\n    try:\n        from chromadb.errors import NotFoundError\n    except ImportError:\n        # Fallback for older ChromaDB versions that don't have chromadb.errors\n        class NotFoundError(Exception):\n            \"\"\"Fallback NotFoundError for older ChromaDB versions.\"\"\"\n\n            pass\n\n    CHROMADB_AVAILABLE = True\nexcept ImportError:\n    CHROMADB_AVAILABLE = False\n\nfrom vanna.capabilities.agent_memory import (\n    AgentMemory,\n    TextMemory,\n    TextMemorySearchResult,\n    ToolMemory,\n    ToolMemorySearchResult,\n)\nfrom vanna.core.tool import ToolContext\n\n\nclass ChromaAgentMemory(AgentMemory):\n    \"\"\"ChromaDB-based implementation of AgentMemory.\n\n    This implementation uses ChromaDB's PersistentClient to store agent memories\n    on disk, ensuring they persist across application restarts.\n\n    Key Features:\n    - Persistent storage: All memories are automatically saved to disk\n    - Efficient retrieval: Existing collections are loaded without re-initializing\n      embedding functions, avoiding unnecessary model downloads\n    - Flexible embedding: Supports custom embedding functions or uses ChromaDB's\n      default embedding function\n\n    Args:\n        persist_directory: Directory where ChromaDB will store its data.\n                          Defaults to \"./chroma_memory\". Use an absolute path\n                          for production deployments to ensure consistent location\n                          across restarts.\n        collection_name: Name of the ChromaDB collection to use. Multiple agents\n                        can share the same persist_directory with different\n                        collection names.\n        embedding_function: Optional custom embedding function. If not provided,\n                           ChromaDB's DefaultEmbeddingFunction is used (requires\n                           internet connection on first use to download the model).\n                           Once a collection is created, subsequent application\n                           restarts will retrieve the existing collection without\n                           re-downloading the model.\n\n    Example:\n        >>> from vanna.integrations.chromadb import ChromaAgentMemory\n        >>> # Basic usage with defaults\n        >>> memory = ChromaAgentMemory(\n        ...     persist_directory=\"/app/data/chroma\",\n        ...     collection_name=\"my_agent_memory\"\n        ... )\n        >>>\n        >>> # With custom embedding function (e.g., for offline use)\n        >>> from chromadb.utils import embedding_functions\n        >>> ef = embedding_functions.SentenceTransformerEmbeddingFunction()\n        >>> memory = ChromaAgentMemory(\n        ...     persist_directory=\"/app/data/chroma\",\n        ...     embedding_function=ef\n        ... )\n\n    Note:\n        The default embedding function downloads an ONNX model (~80MB) on first use.\n        For air-gapped or offline environments, pre-download the model or provide\n        a custom embedding function.\n\n    Limitation:\n        This class does not validate that an existing Chroma collection was created\n        with the same embedding function as the one configured for the current\n        ``ChromaAgentMemory`` instance. If you reuse a collection (same\n        ``persist_directory`` and ``collection_name``) with a different embedding\n        function than was originally used, queries may fail or produce incorrect\n        similarity results. It is your responsibility to ensure that a given\n        collection is always accessed with a consistent embedding function, or to\n        implement your own validation around collection creation and reuse.\n    \"\"\"\n\n    def __init__(\n        self,\n        persist_directory: str = \"./chroma_memory\",\n        collection_name: str = \"tool_memories\",\n        embedding_function=None,\n    ):\n        if not CHROMADB_AVAILABLE:\n            raise ImportError(\n                \"ChromaDB is required for ChromaAgentMemory. Install with: pip install chromadb\"\n            )\n\n        self.persist_directory = persist_directory\n        self.collection_name = collection_name\n        self._client = None\n        self._collection = None\n        self._executor = ThreadPoolExecutor(max_workers=2)\n        self._embedding_function = embedding_function\n\n    def _get_client(self):\n        \"\"\"Get or create ChromaDB client.\"\"\"\n        if self._client is None:\n            self._client = chromadb.PersistentClient(\n                path=self.persist_directory,\n                settings=Settings(anonymized_telemetry=False, allow_reset=True),\n            )\n        return self._client\n\n    def _get_embedding_function(self):\n        \"\"\"Get or create the embedding function.\n\n        If no embedding function was provided during initialization,\n        uses ChromaDB's default embedding function.\n        \"\"\"\n        if self._embedding_function is None:\n            # Use ChromaDB's default embedding function\n            # This avoids requiring sentence-transformers as a hard dependency\n            self._embedding_function = embedding_functions.DefaultEmbeddingFunction()\n        return self._embedding_function\n\n    def _get_collection(self):\n        \"\"\"Get or create ChromaDB collection.\"\"\"\n        if self._collection is None:\n            client = self._get_client()\n            try:\n                # Try to get existing collection first\n                # Don't pass embedding_function here to avoid re-instantiating/downloading it\n                # For existing collections, ChromaDB uses the stored embedding function configuration\n                self._collection = client.get_collection(name=self.collection_name)\n            except NotFoundError:\n                # Collection doesn't exist, create it with embedding function\n                embedding_func = self._get_embedding_function()\n                self._collection = client.create_collection(\n                    name=self.collection_name,\n                    embedding_function=embedding_func,\n                    metadata={\"description\": \"Tool usage memories for learning\"},\n                )\n        return self._collection\n\n    def _create_memory_id(self) -> str:\n        \"\"\"Create a unique ID for a memory.\"\"\"\n        import uuid\n\n        return str(uuid.uuid4())\n\n    async def save_tool_usage(\n        self,\n        question: str,\n        tool_name: str,\n        args: Dict[str, Any],\n        context: ToolContext,\n        success: bool = True,\n        metadata: Optional[Dict[str, Any]] = None,\n    ) -> None:\n        \"\"\"Save a tool usage pattern.\"\"\"\n\n        def _save():\n            collection = self._get_collection()\n\n            memory_id = self._create_memory_id()\n            timestamp = datetime.now().isoformat()\n\n            # ChromaDB only accepts primitive types in metadata\n            # Serialize complex objects to JSON strings\n            memory_data = {\n                \"question\": question,\n                \"tool_name\": tool_name,\n                \"args_json\": json.dumps(args),  # Serialize to JSON string\n                \"timestamp\": timestamp,\n                \"success\": success,\n                \"metadata_json\": json.dumps(metadata or {}),  # Serialize metadata too\n            }\n\n            # Use question as document text for embedding\n            collection.upsert(\n                ids=[memory_id], documents=[question], metadatas=[memory_data]\n            )\n\n        await asyncio.get_event_loop().run_in_executor(self._executor, _save)\n\n    async def search_similar_usage(\n        self,\n        question: str,\n        context: ToolContext,\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n        tool_name_filter: Optional[str] = None,\n    ) -> List[ToolMemorySearchResult]:\n        \"\"\"Search for similar tool usage patterns.\"\"\"\n\n        def _search():\n            collection = self._get_collection()\n\n            # Prepare where filter - ChromaDB requires $and for multiple conditions\n            if tool_name_filter:\n                where_filter = {\n                    \"$and\": [{\"success\": True}, {\"tool_name\": tool_name_filter}]\n                }\n            else:\n                where_filter = {\"success\": True}\n\n            results = collection.query(\n                query_texts=[question], n_results=limit, where=where_filter\n            )\n\n            search_results = []\n            if results[\"ids\"] and len(results[\"ids\"][0]) > 0:\n                for i, (id_, distance, metadata) in enumerate(\n                    zip(\n                        results[\"ids\"][0],\n                        results[\"distances\"][0],\n                        results[\"metadatas\"][0],\n                    )\n                ):\n                    # Convert distance to similarity score (ChromaDB uses L2 distance)\n                    similarity_score = max(0, 1 - distance)\n\n                    if similarity_score >= similarity_threshold:\n                        # Deserialize JSON fields\n                        args = json.loads(metadata.get(\"args_json\", \"{}\"))\n                        metadata_dict = json.loads(metadata.get(\"metadata_json\", \"{}\"))\n\n                        # Use the ChromaDB document ID as the memory ID\n                        memory = ToolMemory(\n                            memory_id=id_,\n                            question=metadata[\"question\"],\n                            tool_name=metadata[\"tool_name\"],\n                            args=args,\n                            timestamp=metadata.get(\"timestamp\"),\n                            success=metadata.get(\"success\", True),\n                            metadata=metadata_dict,\n                        )\n\n                        search_results.append(\n                            ToolMemorySearchResult(\n                                memory=memory,\n                                similarity_score=similarity_score,\n                                rank=i + 1,\n                            )\n                        )\n\n            return search_results\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _search)\n\n    async def get_recent_memories(\n        self, context: ToolContext, limit: int = 10\n    ) -> List[ToolMemory]:\n        \"\"\"Get recently added memories. Returns most recent memories first.\"\"\"\n\n        def _get_recent():\n            collection = self._get_collection()\n\n            # Get all memories and sort by timestamp\n            results = collection.get()\n\n            if not results[\"metadatas\"] or not results[\"ids\"]:\n                return []\n\n            # Parse and sort by timestamp\n            memories_with_time = []\n            for i, (doc_id, metadata) in enumerate(\n                zip(results[\"ids\"], results[\"metadatas\"])\n            ):\n                # Skip text memories - they have is_text_memory flag\n                if metadata.get(\"is_text_memory\"):\n                    continue\n\n                args = json.loads(metadata.get(\"args_json\", \"{}\"))\n                metadata_dict = json.loads(metadata.get(\"metadata_json\", \"{}\"))\n\n                # Use the ChromaDB document ID as the memory ID\n                memory = ToolMemory(\n                    memory_id=doc_id,\n                    question=metadata[\"question\"],\n                    tool_name=metadata[\"tool_name\"],\n                    args=args,\n                    timestamp=metadata.get(\"timestamp\"),\n                    success=metadata.get(\"success\", True),\n                    metadata=metadata_dict,\n                )\n                memories_with_time.append((memory, metadata.get(\"timestamp\", \"\")))\n\n            # Sort by timestamp descending (most recent first)\n            memories_with_time.sort(key=lambda x: x[1], reverse=True)\n\n            # Return only the memory objects, limited to the requested amount\n            return [m[0] for m in memories_with_time[:limit]]\n\n        return await asyncio.get_event_loop().run_in_executor(\n            self._executor, _get_recent\n        )\n\n    async def delete_by_id(self, context: ToolContext, memory_id: str) -> bool:\n        \"\"\"Delete a memory by its ID. Returns True if deleted, False if not found.\"\"\"\n\n        def _delete():\n            collection = self._get_collection()\n\n            # Check if the ID exists\n            try:\n                results = collection.get(ids=[memory_id])\n                if results[\"ids\"] and len(results[\"ids\"]) > 0:\n                    collection.delete(ids=[memory_id])\n                    return True\n                return False\n            except Exception:\n                return False\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _delete)\n\n    async def save_text_memory(self, content: str, context: ToolContext) -> TextMemory:\n        \"\"\"Save a text memory.\"\"\"\n\n        def _save():\n            collection = self._get_collection()\n\n            memory_id = self._create_memory_id()\n            timestamp = datetime.now().isoformat()\n\n            memory_data = {\n                \"content\": content,\n                \"timestamp\": timestamp,\n                \"is_text_memory\": True,\n            }\n\n            collection.upsert(\n                ids=[memory_id], documents=[content], metadatas=[memory_data]\n            )\n\n            return TextMemory(memory_id=memory_id, content=content, timestamp=timestamp)\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _save)\n\n    async def search_text_memories(\n        self,\n        query: str,\n        context: ToolContext,\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n    ) -> List[TextMemorySearchResult]:\n        \"\"\"Search for similar text memories.\"\"\"\n\n        def _search():\n            collection = self._get_collection()\n\n            where_filter = {\"is_text_memory\": True}\n\n            results = collection.query(\n                query_texts=[query], n_results=limit, where=where_filter\n            )\n\n            search_results = []\n            if results[\"ids\"] and len(results[\"ids\"][0]) > 0:\n                for i, (id_, distance, metadata) in enumerate(\n                    zip(\n                        results[\"ids\"][0],\n                        results[\"distances\"][0],\n                        results[\"metadatas\"][0],\n                    )\n                ):\n                    similarity_score = max(0, 1 - distance)\n\n                    if similarity_score >= similarity_threshold:\n                        memory = TextMemory(\n                            memory_id=id_,\n                            content=metadata.get(\"content\", \"\"),\n                            timestamp=metadata.get(\"timestamp\"),\n                        )\n\n                        search_results.append(\n                            TextMemorySearchResult(\n                                memory=memory,\n                                similarity_score=similarity_score,\n                                rank=i + 1,\n                            )\n                        )\n\n            return search_results\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _search)\n\n    async def get_recent_text_memories(\n        self, context: ToolContext, limit: int = 10\n    ) -> List[TextMemory]:\n        \"\"\"Get recently added text memories.\"\"\"\n\n        def _get_recent():\n            collection = self._get_collection()\n\n            results = collection.get(where={\"is_text_memory\": True})\n\n            if not results[\"metadatas\"] or not results[\"ids\"]:\n                return []\n\n            memories_with_time = []\n            for doc_id, metadata in zip(results[\"ids\"], results[\"metadatas\"]):\n                memory = TextMemory(\n                    memory_id=doc_id,\n                    content=metadata.get(\"content\", \"\"),\n                    timestamp=metadata.get(\"timestamp\"),\n                )\n                memories_with_time.append((memory, metadata.get(\"timestamp\", \"\")))\n\n            memories_with_time.sort(key=lambda x: x[1], reverse=True)\n\n            return [m[0] for m in memories_with_time[:limit]]\n\n        return await asyncio.get_event_loop().run_in_executor(\n            self._executor, _get_recent\n        )\n\n    async def delete_text_memory(self, context: ToolContext, memory_id: str) -> bool:\n        \"\"\"Delete a text memory by its ID.\"\"\"\n\n        def _delete():\n            collection = self._get_collection()\n\n            try:\n                results = collection.get(ids=[memory_id])\n                if results[\"ids\"] and len(results[\"ids\"]) > 0:\n                    collection.delete(ids=[memory_id])\n                    return True\n                return False\n            except Exception:\n                return False\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _delete)\n\n    async def clear_memories(\n        self,\n        context: ToolContext,\n        tool_name: Optional[str] = None,\n        before_date: Optional[str] = None,\n    ) -> int:\n        \"\"\"Clear stored memories.\"\"\"\n\n        def _clear():\n            collection = self._get_collection()\n\n            # Build where filter\n            where_filter = {}\n            if tool_name:\n                where_filter[\"tool_name\"] = tool_name\n\n            # Get memories to delete\n            results = collection.get(where=where_filter if where_filter else None)\n\n            if not results[\"ids\"]:\n                return 0\n\n            ids_to_delete = []\n            for i, metadata in enumerate(results[\"metadatas\"]):\n                if before_date:\n                    memory_date = metadata.get(\"timestamp\", \"\")\n                    if memory_date and memory_date < before_date:\n                        ids_to_delete.append(results[\"ids\"][i])\n                else:\n                    ids_to_delete.append(results[\"ids\"][i])\n\n            if ids_to_delete:\n                collection.delete(ids=ids_to_delete)\n\n            return len(ids_to_delete)\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _clear)\n"
  },
  {
    "path": "src/vanna/integrations/clickhouse/__init__.py",
    "content": "\"\"\"ClickHouse integration for Vanna.\"\"\"\n\nfrom .sql_runner import ClickHouseRunner\n\n__all__ = [\"ClickHouseRunner\"]\n"
  },
  {
    "path": "src/vanna/integrations/clickhouse/sql_runner.py",
    "content": "\"\"\"ClickHouse implementation of SqlRunner interface.\"\"\"\n\nfrom typing import Optional\nimport pandas as pd\n\nfrom vanna.capabilities.sql_runner import SqlRunner, RunSqlToolArgs\nfrom vanna.core.tool import ToolContext\n\n\nclass ClickHouseRunner(SqlRunner):\n    \"\"\"ClickHouse implementation of the SqlRunner interface.\"\"\"\n\n    def __init__(\n        self,\n        host: str,\n        database: str,\n        user: str,\n        password: str,\n        port: int = 8123,\n        **kwargs,\n    ):\n        \"\"\"Initialize with ClickHouse connection parameters.\n\n        Args:\n            host: Database host address\n            database: Database name\n            user: Database user\n            password: Database password\n            port: Database port (default: 8123)\n            **kwargs: Additional clickhouse_connect connection parameters\n        \"\"\"\n        try:\n            import clickhouse_connect\n\n            self.clickhouse_connect = clickhouse_connect\n        except ImportError as e:\n            raise ImportError(\n                \"clickhouse-connect package is required. \"\n                \"Install with: pip install 'vanna[clickhouse]'\"\n            ) from e\n\n        self.host = host\n        self.port = port\n        self.user = user\n        self.password = password\n        self.database = database\n        self.kwargs = kwargs\n\n    async def run_sql(self, args: RunSqlToolArgs, context: ToolContext) -> pd.DataFrame:\n        \"\"\"Execute SQL query against ClickHouse database and return results as DataFrame.\n\n        Args:\n            args: SQL query arguments\n            context: Tool execution context\n\n        Returns:\n            DataFrame with query results\n\n        Raises:\n            Exception: If query execution fails\n        \"\"\"\n        # Connect to the database\n        client = self.clickhouse_connect.get_client(\n            host=self.host,\n            port=self.port,\n            username=self.user,\n            password=self.password,\n            database=self.database,\n            **self.kwargs,\n        )\n\n        try:\n            # Execute the query\n            result = client.query(args.sql)\n            results = result.result_rows\n\n            # Create a pandas dataframe from the results\n            df = pd.DataFrame(results, columns=result.column_names)\n            return df\n\n        finally:\n            client.close()\n"
  },
  {
    "path": "src/vanna/integrations/duckdb/__init__.py",
    "content": "\"\"\"DuckDB integration for Vanna.\"\"\"\n\nfrom .sql_runner import DuckDBRunner\n\n__all__ = [\"DuckDBRunner\"]\n"
  },
  {
    "path": "src/vanna/integrations/duckdb/sql_runner.py",
    "content": "\"\"\"DuckDB implementation of SqlRunner interface.\"\"\"\n\nfrom typing import Optional\nimport pandas as pd\n\nfrom vanna.capabilities.sql_runner import SqlRunner, RunSqlToolArgs\nfrom vanna.core.tool import ToolContext\n\n\nclass DuckDBRunner(SqlRunner):\n    \"\"\"DuckDB implementation of the SqlRunner interface.\"\"\"\n\n    def __init__(\n        self, database_path: str = \":memory:\", init_sql: Optional[str] = None, **kwargs\n    ):\n        \"\"\"Initialize with DuckDB connection parameters.\n\n        Args:\n            database_path: Path to the DuckDB database file.\n                          Use \":memory:\" for in-memory database (default).\n                          Use \"md:\" or \"motherduck:\" for MotherDuck database.\n            init_sql: Optional SQL to run when connecting to the database\n            **kwargs: Additional duckdb connection parameters\n        \"\"\"\n        try:\n            import duckdb\n\n            self.duckdb = duckdb\n        except ImportError as e:\n            raise ImportError(\n                \"duckdb package is required. Install with: pip install 'vanna[duckdb]'\"\n            ) from e\n\n        self.database_path = database_path\n        self.init_sql = init_sql\n        self.kwargs = kwargs\n        self._conn = None\n\n    def _get_connection(self):\n        \"\"\"Get or create DuckDB connection.\"\"\"\n        if self._conn is None:\n            self._conn = self.duckdb.connect(self.database_path, **self.kwargs)\n            if self.init_sql:\n                self._conn.query(self.init_sql)\n        return self._conn\n\n    async def run_sql(self, args: RunSqlToolArgs, context: ToolContext) -> pd.DataFrame:\n        \"\"\"Execute SQL query against DuckDB database and return results as DataFrame.\n\n        Args:\n            args: SQL query arguments\n            context: Tool execution context\n\n        Returns:\n            DataFrame with query results\n\n        Raises:\n            duckdb.Error: If query execution fails\n        \"\"\"\n        conn = self._get_connection()\n\n        # Execute the query and convert to DataFrame\n        df = conn.query(args.sql).to_df()\n\n        return df\n"
  },
  {
    "path": "src/vanna/integrations/faiss/__init__.py",
    "content": "\"\"\"\nFAISS integration for Vanna Agents.\n\"\"\"\n\nfrom .agent_memory import FAISSAgentMemory\n\n__all__ = [\"FAISSAgentMemory\"]\n"
  },
  {
    "path": "src/vanna/integrations/faiss/agent_memory.py",
    "content": "\"\"\"\nFAISS vector database implementation of AgentMemory.\n\nThis implementation uses FAISS for local vector storage of tool usage patterns.\n\"\"\"\n\nimport json\nimport uuid\nimport pickle\nimport os\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional\nimport asyncio\nfrom concurrent.futures import ThreadPoolExecutor\nimport numpy as np\n\ntry:\n    import faiss\n\n    FAISS_AVAILABLE = True\nexcept ImportError:\n    FAISS_AVAILABLE = False\n\nfrom vanna.capabilities.agent_memory import (\n    AgentMemory,\n    TextMemory,\n    TextMemorySearchResult,\n    ToolMemory,\n    ToolMemorySearchResult,\n)\nfrom vanna.core.tool import ToolContext\n\n\nclass FAISSAgentMemory(AgentMemory):\n    \"\"\"FAISS-based implementation of AgentMemory.\"\"\"\n\n    def __init__(\n        self,\n        index_path: Optional[str] = None,\n        persist_path: Optional[str] = None,\n        dimension: int = 384,\n        metric: str = \"cosine\",\n    ):\n        if not FAISS_AVAILABLE:\n            raise ImportError(\n                \"FAISS is required for FAISSAgentMemory. Install with: pip install faiss-cpu\"\n            )\n\n        # Accept either index_path or persist_path for backward compatibility\n        self.index_path = persist_path or index_path or \"./faiss_index\"\n        self.dimension = dimension\n        self.metric = metric\n        self._index = None\n        self._metadata = {}\n        self._executor = ThreadPoolExecutor(max_workers=2)\n        self._load_index()\n\n    def _load_index(self):\n        \"\"\"Load or create FAISS index.\"\"\"\n        index_file = os.path.join(self.index_path, \"index.faiss\")\n        metadata_file = os.path.join(self.index_path, \"metadata.pkl\")\n\n        if os.path.exists(index_file) and os.path.exists(metadata_file):\n            # Load existing index\n            self._index = faiss.read_index(index_file)\n            with open(metadata_file, \"rb\") as f:\n                self._metadata = pickle.load(f)\n        else:\n            # Create new index\n            os.makedirs(self.index_path, exist_ok=True)\n            if self.metric == \"cosine\":\n                self._index = faiss.IndexFlatIP(self.dimension)\n            else:\n                self._index = faiss.IndexFlatL2(self.dimension)\n            self._metadata = {}\n\n    def _save_index(self):\n        \"\"\"Save FAISS index to disk.\"\"\"\n        index_file = os.path.join(self.index_path, \"index.faiss\")\n        metadata_file = os.path.join(self.index_path, \"metadata.pkl\")\n\n        faiss.write_index(self._index, index_file)\n        with open(metadata_file, \"wb\") as f:\n            pickle.dump(self._metadata, f)\n\n    def _create_embedding(self, text: str) -> np.ndarray:\n        \"\"\"Create a simple embedding from text (placeholder).\"\"\"\n        import hashlib\n\n        hash_val = int(hashlib.md5(text.encode()).hexdigest(), 16)\n        embedding = np.array(\n            [(hash_val >> i) % 100 / 100.0 for i in range(self.dimension)],\n            dtype=np.float32,\n        )\n\n        # Normalize for cosine similarity\n        if self.metric == \"cosine\":\n            norm = np.linalg.norm(embedding)\n            if norm > 0:\n                embedding = embedding / norm\n\n        return embedding\n\n    async def save_tool_usage(\n        self,\n        question: str,\n        tool_name: str,\n        args: Dict[str, Any],\n        context: ToolContext,\n        success: bool = True,\n        metadata: Optional[Dict[str, Any]] = None,\n    ) -> None:\n        \"\"\"Save a tool usage pattern.\"\"\"\n\n        def _save():\n            memory_id = str(uuid.uuid4())\n            timestamp = datetime.now().isoformat()\n            embedding = self._create_embedding(question)\n\n            # Add to FAISS index\n            self._index.add(np.array([embedding]))\n\n            # Store metadata\n            idx = self._index.ntotal - 1\n            self._metadata[idx] = {\n                \"memory_id\": memory_id,\n                \"question\": question,\n                \"tool_name\": tool_name,\n                \"args\": args,\n                \"timestamp\": timestamp,\n                \"success\": success,\n                \"metadata\": metadata or {},\n            }\n\n            self._save_index()\n\n        await asyncio.get_event_loop().run_in_executor(self._executor, _save)\n\n    async def search_similar_usage(\n        self,\n        question: str,\n        context: ToolContext,\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n        tool_name_filter: Optional[str] = None,\n    ) -> List[ToolMemorySearchResult]:\n        \"\"\"Search for similar tool usage patterns.\"\"\"\n\n        def _search():\n            embedding = self._create_embedding(question)\n\n            # Search in FAISS\n            k = min(limit * 3, self._index.ntotal) if self._index.ntotal > 0 else 1\n            if k == 0:\n                return []\n\n            distances, indices = self._index.search(np.array([embedding]), k)\n\n            search_results = []\n            rank = 1\n            for i, (dist, idx) in enumerate(zip(distances[0], indices[0])):\n                if idx == -1 or idx not in self._metadata:\n                    continue\n\n                metadata = self._metadata[idx]\n\n                # Filter by success\n                if not metadata.get(\"success\", True):\n                    continue\n\n                # Filter by tool name\n                if tool_name_filter and metadata.get(\"tool_name\") != tool_name_filter:\n                    continue\n\n                # Convert distance to similarity score\n                if self.metric == \"cosine\":\n                    similarity_score = float(dist)\n                else:\n                    similarity_score = 1.0 / (1.0 + float(dist))\n\n                if similarity_score >= similarity_threshold:\n                    memory = ToolMemory(\n                        memory_id=metadata[\"memory_id\"],\n                        question=metadata[\"question\"],\n                        tool_name=metadata[\"tool_name\"],\n                        args=metadata[\"args\"],\n                        timestamp=metadata.get(\"timestamp\"),\n                        success=metadata.get(\"success\", True),\n                        metadata=metadata.get(\"metadata\", {}),\n                    )\n\n                    search_results.append(\n                        ToolMemorySearchResult(\n                            memory=memory, similarity_score=similarity_score, rank=rank\n                        )\n                    )\n                    rank += 1\n\n                    if len(search_results) >= limit:\n                        break\n\n            return search_results\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _search)\n\n    async def get_recent_memories(\n        self, context: ToolContext, limit: int = 10\n    ) -> List[ToolMemory]:\n        \"\"\"Get recently added memories.\"\"\"\n\n        def _get_recent():\n            # Get all metadata entries and sort by timestamp\n            all_entries = list(self._metadata.values())\n            sorted_entries = sorted(\n                all_entries, key=lambda m: m.get(\"timestamp\", \"\"), reverse=True\n            )\n\n            memories = []\n            for entry in sorted_entries[:limit]:\n                # Skip text memories - they have is_text_memory flag\n                if entry.get(\"is_text_memory\"):\n                    continue\n\n                memory = ToolMemory(\n                    memory_id=entry[\"memory_id\"],\n                    question=entry[\"question\"],\n                    tool_name=entry[\"tool_name\"],\n                    args=entry[\"args\"],\n                    timestamp=entry.get(\"timestamp\"),\n                    success=entry.get(\"success\", True),\n                    metadata=entry.get(\"metadata\", {}),\n                )\n                memories.append(memory)\n\n            return memories\n\n        return await asyncio.get_event_loop().run_in_executor(\n            self._executor, _get_recent\n        )\n\n    async def delete_by_id(self, context: ToolContext, memory_id: str) -> bool:\n        \"\"\"Delete a memory by its ID.\"\"\"\n\n        def _delete():\n            # Find and remove from metadata\n            idx_to_remove = None\n            for idx, metadata in self._metadata.items():\n                if metadata[\"memory_id\"] == memory_id:\n                    idx_to_remove = idx\n                    break\n\n            if idx_to_remove is not None:\n                del self._metadata[idx_to_remove]\n                self._save_index()\n                return True\n\n            return False\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _delete)\n\n    async def save_text_memory(self, content: str, context: ToolContext) -> TextMemory:\n        \"\"\"Save a text memory.\"\"\"\n\n        def _save():\n            memory_id = str(uuid.uuid4())\n            timestamp = datetime.now().isoformat()\n            embedding = self._create_embedding(content)\n\n            # Add to FAISS index\n            self._index.add(np.array([embedding]))\n\n            # Store metadata\n            idx = self._index.ntotal - 1\n            self._metadata[idx] = {\n                \"memory_id\": memory_id,\n                \"content\": content,\n                \"timestamp\": timestamp,\n                \"is_text_memory\": True,\n            }\n\n            self._save_index()\n\n            return TextMemory(memory_id=memory_id, content=content, timestamp=timestamp)\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _save)\n\n    async def search_text_memories(\n        self,\n        query: str,\n        context: ToolContext,\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n    ) -> List[TextMemorySearchResult]:\n        \"\"\"Search for similar text memories.\"\"\"\n\n        def _search():\n            embedding = self._create_embedding(query)\n\n            k = min(limit * 3, self._index.ntotal) if self._index.ntotal > 0 else 1\n            if k == 0:\n                return []\n\n            distances, indices = self._index.search(np.array([embedding]), k)\n\n            search_results = []\n            rank = 1\n            for dist, idx in zip(distances[0], indices[0]):\n                if idx == -1 or idx not in self._metadata:\n                    continue\n\n                metadata = self._metadata[idx]\n\n                # Filter for text memories only\n                if not metadata.get(\"is_text_memory\", False):\n                    continue\n\n                # Convert distance to similarity score\n                if self.metric == \"cosine\":\n                    similarity_score = float(dist)\n                else:\n                    similarity_score = 1.0 / (1.0 + float(dist))\n\n                if similarity_score >= similarity_threshold:\n                    memory = TextMemory(\n                        memory_id=metadata[\"memory_id\"],\n                        content=metadata[\"content\"],\n                        timestamp=metadata.get(\"timestamp\"),\n                    )\n\n                    search_results.append(\n                        TextMemorySearchResult(\n                            memory=memory, similarity_score=similarity_score, rank=rank\n                        )\n                    )\n                    rank += 1\n\n                    if len(search_results) >= limit:\n                        break\n\n            return search_results\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _search)\n\n    async def get_recent_text_memories(\n        self, context: ToolContext, limit: int = 10\n    ) -> List[TextMemory]:\n        \"\"\"Get recently added text memories.\"\"\"\n\n        def _get_recent():\n            # Get all text memory entries and sort by timestamp\n            text_entries = [\n                entry\n                for entry in self._metadata.values()\n                if entry.get(\"is_text_memory\", False)\n            ]\n            sorted_entries = sorted(\n                text_entries, key=lambda m: m.get(\"timestamp\", \"\"), reverse=True\n            )\n\n            memories = []\n            for entry in sorted_entries[:limit]:\n                memory = TextMemory(\n                    memory_id=entry[\"memory_id\"],\n                    content=entry[\"content\"],\n                    timestamp=entry.get(\"timestamp\"),\n                )\n                memories.append(memory)\n\n            return memories\n\n        return await asyncio.get_event_loop().run_in_executor(\n            self._executor, _get_recent\n        )\n\n    async def delete_text_memory(self, context: ToolContext, memory_id: str) -> bool:\n        \"\"\"Delete a text memory by its ID.\"\"\"\n\n        def _delete():\n            # Find and remove from metadata\n            idx_to_remove = None\n            for idx, metadata in self._metadata.items():\n                if metadata[\"memory_id\"] == memory_id:\n                    idx_to_remove = idx\n                    break\n\n            if idx_to_remove is not None:\n                del self._metadata[idx_to_remove]\n                self._save_index()\n                return True\n\n            return False\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _delete)\n\n    async def clear_memories(\n        self,\n        context: ToolContext,\n        tool_name: Optional[str] = None,\n        before_date: Optional[str] = None,\n    ) -> int:\n        \"\"\"Clear stored memories.\"\"\"\n\n        def _clear():\n            indices_to_remove = []\n\n            for idx, metadata in self._metadata.items():\n                should_remove = True\n\n                if tool_name and metadata.get(\"tool_name\") != tool_name:\n                    should_remove = False\n\n                if before_date and metadata.get(\"timestamp\", \"\") >= before_date:\n                    should_remove = False\n\n                if should_remove:\n                    indices_to_remove.append(idx)\n\n            # Remove from metadata\n            for idx in indices_to_remove:\n                del self._metadata[idx]\n\n            # If clearing all, recreate index\n            if not tool_name and not before_date:\n                if self.metric == \"cosine\":\n                    self._index = faiss.IndexFlatIP(self.dimension)\n                else:\n                    self._index = faiss.IndexFlatL2(self.dimension)\n                self._metadata = {}\n\n            self._save_index()\n            return len(indices_to_remove)\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _clear)\n"
  },
  {
    "path": "src/vanna/integrations/google/__init__.py",
    "content": "\"\"\"\nGoogle AI integrations.\n\nThis module provides Google AI service implementations.\n\"\"\"\n\nfrom .gemini import GeminiLlmService\n\n__all__ = [\"GeminiLlmService\"]\n"
  },
  {
    "path": "src/vanna/integrations/google/gemini.py",
    "content": "\"\"\"\nGoogle Gemini LLM service implementation.\n\nImplements the LlmService interface using Google's Gen AI SDK\n(google-genai). Supports non-streaming and streaming text output,\nas well as function calling (tool use).\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport logging\nimport os\nfrom typing import Any, AsyncGenerator, Dict, List, Optional\n\nlogger = logging.getLogger(__name__)\n\nfrom vanna.core.llm import (\n    LlmService,\n    LlmRequest,\n    LlmResponse,\n    LlmStreamChunk,\n)\nfrom vanna.core.tool import ToolCall, ToolSchema\n\n\nclass GeminiLlmService(LlmService):\n    \"\"\"Google Gemini-backed LLM service.\n\n    Args:\n        model: Gemini model name (e.g., \"gemini-2.5-pro\", \"gemini-2.5-flash\").\n            Defaults to \"gemini-2.5-pro\". Can also be set via GEMINI_MODEL env var.\n        api_key: API key; falls back to env `GOOGLE_API_KEY` or `GEMINI_API_KEY`.\n            GOOGLE_API_KEY takes precedence if both are set.\n        temperature: Temperature for generation (0.0-2.0). Default 0.7.\n        extra_config: Extra kwargs forwarded to GenerateContentConfig.\n    \"\"\"\n\n    def __init__(\n        self,\n        model: Optional[str] = None,\n        api_key: Optional[str] = None,\n        temperature: float = 0.7,\n        **extra_config: Any,\n    ) -> None:\n        try:\n            from google import genai\n            from google.genai import types\n        except Exception as e:  # pragma: no cover\n            raise ImportError(\n                \"google-genai package is required. \"\n                \"Install with: pip install 'vanna[gemini]'\"\n            ) from e\n\n        self.model_name = model or os.getenv(\"GEMINI_MODEL\", \"gemini-2.5-pro\")\n        # Check GOOGLE_API_KEY first (takes precedence), then GEMINI_API_KEY\n        api_key = api_key or os.getenv(\"GOOGLE_API_KEY\") or os.getenv(\"GEMINI_API_KEY\")\n\n        if not api_key:\n            raise ValueError(\n                \"Google API key is required. Set GOOGLE_API_KEY or GEMINI_API_KEY \"\n                \"environment variable, or pass api_key parameter.\"\n            )\n\n        # Store modules for use in methods\n        self._genai = genai\n        self._types = types\n\n        # Create client\n        self._client = genai.Client(api_key=api_key)\n\n        # Store generation config\n        self.temperature = temperature\n        self.extra_config = extra_config\n\n    async def send_request(self, request: LlmRequest) -> LlmResponse:\n        \"\"\"Send a non-streaming request to Gemini and return the response.\"\"\"\n        contents, config = self._build_payload(request)\n\n        try:\n            # Generate content\n            response = self._client.models.generate_content(\n                model=self.model_name,\n                contents=contents,\n                config=config,\n            )\n\n            logger.info(f\"Gemini response: {response}\")\n\n            # Parse response\n            text_content, tool_calls = self._parse_response(response)\n\n            # Extract usage information\n            usage: Dict[str, int] = {}\n            if hasattr(response, \"usage_metadata\"):\n                try:\n                    usage = {\n                        \"prompt_tokens\": int(\n                            response.usage_metadata.prompt_token_count\n                        ),\n                        \"completion_tokens\": int(\n                            response.usage_metadata.candidates_token_count\n                        ),\n                        \"total_tokens\": int(response.usage_metadata.total_token_count),\n                    }\n                except Exception:\n                    pass\n\n            # Get finish reason\n            finish_reason = None\n            if response.candidates:\n                finish_reason = str(response.candidates[0].finish_reason).lower()\n\n            return LlmResponse(\n                content=text_content or None,\n                tool_calls=tool_calls or None,\n                finish_reason=finish_reason,\n                usage=usage or None,\n            )\n\n        except Exception as e:\n            logger.error(f\"Error calling Gemini API: {e}\")\n            raise\n\n    async def stream_request(\n        self, request: LlmRequest\n    ) -> AsyncGenerator[LlmStreamChunk, None]:\n        \"\"\"Stream a request to Gemini.\n\n        Yields text chunks as they arrive. Emits tool calls at the end.\n        \"\"\"\n        contents, config = self._build_payload(request)\n\n        logger.info(f\"Gemini streaming request with model: {self.model_name}\")\n\n        try:\n            # Stream content\n            stream = self._client.models.generate_content_stream(\n                model=self.model_name,\n                contents=contents,\n                config=config,\n            )\n\n            # Accumulate chunks for tool calls\n            accumulated_chunks = []\n\n            for chunk in stream:\n                accumulated_chunks.append(chunk)\n\n                # Yield text content as it arrives\n                if hasattr(chunk, \"text\") and chunk.text:\n                    yield LlmStreamChunk(content=chunk.text)\n\n            # After stream completes, check for tool calls in accumulated response\n            if accumulated_chunks:\n                final_chunk = accumulated_chunks[-1]\n                _, tool_calls = self._parse_response_chunk(final_chunk)\n\n                finish_reason = None\n                if final_chunk.candidates:\n                    finish_reason = str(final_chunk.candidates[0].finish_reason).lower()\n\n                if tool_calls:\n                    yield LlmStreamChunk(\n                        tool_calls=tool_calls,\n                        finish_reason=finish_reason,\n                    )\n                else:\n                    yield LlmStreamChunk(finish_reason=finish_reason or \"stop\")\n\n        except Exception as e:\n            logger.error(f\"Error streaming from Gemini API: {e}\")\n            raise\n\n    async def validate_tools(self, tools: List[ToolSchema]) -> List[str]:\n        \"\"\"Basic validation of tool schemas for Gemini.\"\"\"\n        errors: List[str] = []\n        for t in tools:\n            if not t.name:\n                errors.append(\"Tool name is required\")\n            if not t.description:\n                errors.append(f\"Tool {t.name}: description is required\")\n        return errors\n\n    # Internal helpers\n    def _build_payload(self, request: LlmRequest) -> tuple[List[Any], Any]:\n        \"\"\"Build the payload for Gemini API.\n\n        Returns:\n            Tuple of (contents, config)\n        \"\"\"\n        # Build contents (messages) for Gemini\n        contents = []\n\n        # System prompt handling - Gemini supports system instructions in config\n        system_instruction = None\n        if request.system_prompt:\n            system_instruction = request.system_prompt\n\n        for m in request.messages:\n            # Map roles: user -> user, assistant -> model, tool -> function\n            if m.role == \"user\":\n                contents.append(\n                    self._types.Content(\n                        role=\"user\", parts=[self._types.Part(text=m.content)]\n                    )\n                )\n            elif m.role == \"assistant\":\n                parts = []\n\n                # Add text content if present\n                if m.content and m.content.strip():\n                    parts.append(self._types.Part(text=m.content))\n\n                # Add tool calls if present\n                if m.tool_calls:\n                    for tc in m.tool_calls:\n                        parts.append(\n                            self._types.Part(\n                                function_call=self._types.FunctionCall(\n                                    name=tc.name, args=tc.arguments\n                                )\n                            )\n                        )\n\n                if parts:\n                    contents.append(self._types.Content(role=\"model\", parts=parts))\n\n            elif m.role == \"tool\":\n                # Tool results in Gemini format\n                if m.tool_call_id:\n                    # Parse the content as JSON if possible\n                    try:\n                        response_content = json.loads(m.content)\n                    except (json.JSONDecodeError, TypeError):\n                        response_content = {\"result\": m.content}\n\n                    # Extract function name from tool_call_id or use a default\n                    function_name = m.tool_call_id.replace(\"call_\", \"\")\n\n                    contents.append(\n                        self._types.Content(\n                            role=\"function\",\n                            parts=[\n                                self._types.Part(\n                                    function_response=self._types.FunctionResponse(\n                                        name=function_name, response=response_content\n                                    )\n                                )\n                            ],\n                        )\n                    )\n\n        # Build tools configuration if tools are provided\n        tools = None\n        if request.tools:\n            function_declarations = []\n            for tool in request.tools:\n                # Clean schema to remove unsupported fields\n                cleaned_parameters = self._clean_schema_for_gemini(tool.parameters)\n\n                function_declarations.append(\n                    {\n                        \"name\": tool.name,\n                        \"description\": tool.description,\n                        \"parameters\": cleaned_parameters,\n                    }\n                )\n\n            if function_declarations:\n                tools = [self._types.Tool(function_declarations=function_declarations)]\n\n        # Build generation config\n        config_dict = {\n            \"temperature\": request.temperature,\n            **self.extra_config,\n        }\n\n        if request.max_tokens is not None:\n            config_dict[\"max_output_tokens\"] = request.max_tokens\n\n        if tools:\n            config_dict[\"tools\"] = tools\n\n        if system_instruction:\n            config_dict[\"system_instruction\"] = system_instruction\n\n        config = self._types.GenerateContentConfig(**config_dict)\n\n        return contents, config\n\n    def _parse_response(self, response: Any) -> tuple[str, List[ToolCall]]:\n        \"\"\"Parse a Gemini response into text and tool calls.\"\"\"\n        text_parts: List[str] = []\n        tool_calls: List[ToolCall] = []\n\n        if not response.candidates:\n            return \"\", []\n\n        candidate = response.candidates[0]\n\n        if (\n            hasattr(candidate, \"content\")\n            and candidate.content\n            and hasattr(candidate.content, \"parts\")\n            and candidate.content.parts\n        ):\n            for part in candidate.content.parts:\n                # Check for text content\n                if hasattr(part, \"text\") and part.text:\n                    text_parts.append(part.text)\n\n                # Check for function calls\n                if hasattr(part, \"function_call\") and part.function_call:\n                    fc = part.function_call\n                    # Convert function call to ToolCall\n                    tool_calls.append(\n                        ToolCall(\n                            id=f\"call_{fc.name}\",  # Generate an ID\n                            name=fc.name,\n                            arguments=dict(fc.args) if hasattr(fc, \"args\") else {},\n                        )\n                    )\n\n        text_content = \"\".join(text_parts)\n        return text_content, tool_calls\n\n    def _parse_response_chunk(self, chunk: Any) -> tuple[str, List[ToolCall]]:\n        \"\"\"Parse a streaming chunk (same logic as _parse_response).\"\"\"\n        return self._parse_response(chunk)\n\n    def _clean_schema_for_gemini(self, schema: Dict[str, Any]) -> Dict[str, Any]:\n        \"\"\"Clean JSON Schema to only include fields supported by Gemini.\n\n        Gemini only supports a subset of OpenAPI schema. This removes unsupported\n        fields like 'title', 'default', '$schema', etc.\n\n        Supported fields:\n        - type, description, enum\n        - properties, required, items (for objects/arrays)\n        \"\"\"\n        if not isinstance(schema, dict):\n            return schema\n\n        # Fields that Gemini supports\n        allowed_fields = {\n            \"type\",\n            \"description\",\n            \"enum\",\n            \"properties\",\n            \"required\",\n            \"items\",\n            \"format\",\n        }\n\n        cleaned = {}\n        for key, value in schema.items():\n            if key in allowed_fields:\n                # Recursively clean nested schemas\n                if key == \"properties\" and isinstance(value, dict):\n                    cleaned[key] = {\n                        prop_name: self._clean_schema_for_gemini(prop_schema)\n                        for prop_name, prop_schema in value.items()\n                    }\n                elif key == \"items\" and isinstance(value, dict):\n                    cleaned[key] = self._clean_schema_for_gemini(value)\n                else:\n                    cleaned[key] = value\n\n        return cleaned\n"
  },
  {
    "path": "src/vanna/integrations/hive/__init__.py",
    "content": "\"\"\"Hive integration for Vanna.\"\"\"\n\nfrom .sql_runner import HiveRunner\n\n__all__ = [\"HiveRunner\"]\n"
  },
  {
    "path": "src/vanna/integrations/hive/sql_runner.py",
    "content": "\"\"\"Hive implementation of SqlRunner interface.\"\"\"\n\nfrom typing import Optional\nimport pandas as pd\n\nfrom vanna.capabilities.sql_runner import SqlRunner, RunSqlToolArgs\nfrom vanna.core.tool import ToolContext\n\n\nclass HiveRunner(SqlRunner):\n    \"\"\"Hive implementation of the SqlRunner interface.\"\"\"\n\n    def __init__(\n        self,\n        host: str,\n        database: str = \"default\",\n        user: Optional[str] = None,\n        password: Optional[str] = None,\n        port: int = 10000,\n        auth: str = \"CUSTOM\",\n        **kwargs,\n    ):\n        \"\"\"Initialize with Hive connection parameters.\n\n        Args:\n            host: The host of the Hive database\n            database: The name of the database to connect to (default: 'default')\n            user: The username to use for authentication\n            password: The password to use for authentication\n            port: The port to use for the connection (default: 10000)\n            auth: The authentication method to use (default: 'CUSTOM')\n            **kwargs: Additional pyhive connection parameters\n        \"\"\"\n        try:\n            from pyhive import hive\n\n            self.hive = hive\n        except ImportError as e:\n            raise ImportError(\n                \"pyhive package is required. Install with: pip install pyhive\"\n            ) from e\n\n        self.host = host\n        self.database = database\n        self.user = user\n        self.password = password\n        self.port = port\n        self.auth = auth\n        self.kwargs = kwargs\n\n    async def run_sql(self, args: RunSqlToolArgs, context: ToolContext) -> pd.DataFrame:\n        \"\"\"Execute SQL query against Hive database and return results as DataFrame.\n\n        Args:\n            args: SQL query arguments\n            context: Tool execution context\n\n        Returns:\n            DataFrame with query results\n\n        Raises:\n            hive.Error: If query execution fails\n        \"\"\"\n        # Connect to the database\n        conn = self.hive.Connection(\n            host=self.host,\n            username=self.user,\n            password=self.password,\n            database=self.database,\n            port=self.port,\n            auth=self.auth,\n            **self.kwargs,\n        )\n\n        try:\n            cursor = conn.cursor()\n            cursor.execute(args.sql)\n            results = cursor.fetchall()\n\n            # Create a pandas dataframe from the results\n            df = pd.DataFrame(results, columns=[desc[0] for desc in cursor.description])\n\n            cursor.close()\n            return df\n\n        finally:\n            conn.close()\n"
  },
  {
    "path": "src/vanna/integrations/local/__init__.py",
    "content": "\"\"\"\nLocal integration.\n\nThis module provides built-in local implementations.\n\"\"\"\n\nfrom .audit import LoggingAuditLogger\nfrom .file_system import LocalFileSystem\nfrom .storage import MemoryConversationStore\nfrom .file_system_conversation_store import FileSystemConversationStore\n\n__all__ = [\n    \"MemoryConversationStore\",\n    \"FileSystemConversationStore\",\n    \"LocalFileSystem\",\n    \"LoggingAuditLogger\",\n]\n"
  },
  {
    "path": "src/vanna/integrations/local/agent_memory/__init__.py",
    "content": "\"\"\"\nLocal agent memory implementations.\n\"\"\"\n\nfrom .in_memory import DemoAgentMemory\n\n__all__ = [\"DemoAgentMemory\"]\n"
  },
  {
    "path": "src/vanna/integrations/local/agent_memory/in_memory.py",
    "content": "\"\"\"\nDemo in-memory implementation of AgentMemory.\n\nThis implementation provides a zero-dependency, minimal storage solution that\nkeeps all memories in RAM. It uses simple similarity algorithms (Jaccard and\ndifflib) instead of vector embeddings. Perfect for demos and testing.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport difflib\nimport time\nimport uuid\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional\n\nfrom vanna.capabilities.agent_memory import (\n    AgentMemory,\n    TextMemory,\n    TextMemorySearchResult,\n    ToolMemory,\n    ToolMemorySearchResult,\n)\nfrom vanna.core.tool import ToolContext\n\n\nclass DemoAgentMemory(AgentMemory):\n    \"\"\"\n    Minimal, dependency-free in-memory storage for demos and testing.\n    - O(n) search over an in-memory list\n    - Simple similarity: max(Jaccard(token sets), difflib ratio)\n    - Optional FIFO eviction via max_items\n    - Async-safe with an asyncio.Lock\n    \"\"\"\n\n    def __init__(self, *, max_items: int = 10_000):\n        \"\"\"\n        Initialize the in-memory storage.\n\n        Args:\n            max_items: Maximum number of memories to keep. Oldest memories are\n                      evicted when this limit is reached (FIFO).\n        \"\"\"\n        self._memories: List[ToolMemory] = []\n        self._text_memories: List[TextMemory] = []\n        self._lock = asyncio.Lock()\n        self._max_items = max_items\n\n    @staticmethod\n    def _now_iso() -> str:\n        \"\"\"Get current timestamp in ISO format.\"\"\"\n        return datetime.now().isoformat()\n\n    @staticmethod\n    def _normalize(text: str) -> str:\n        \"\"\"Normalize text by lowercasing and collapsing whitespace.\"\"\"\n        return \" \".join(text.lower().split())\n\n    @staticmethod\n    def _tokenize(text: str) -> set[str]:\n        \"\"\"Simple tokenizer that splits on whitespace.\"\"\"\n        return set(text.lower().split())\n\n    @classmethod\n    def _similarity(cls, a: str, b: str) -> float:\n        \"\"\"\n        Calculate similarity between two strings using multiple methods.\n\n        Returns the maximum of Jaccard similarity and difflib ratio.\n        \"\"\"\n        a_norm, b_norm = cls._normalize(a), cls._normalize(b)\n\n        # Jaccard over whitespace tokens\n        ta, tb = cls._tokenize(a_norm), cls._tokenize(b_norm)\n        if not ta and not tb:\n            jaccard = 1.0\n        elif not ta or not tb:\n            jaccard = 0.0\n        else:\n            jaccard = len(ta & tb) / max(1, len(ta | tb))\n\n        # difflib ratio\n        ratio = difflib.SequenceMatcher(None, a_norm, b_norm).ratio()\n\n        # Take the better of the two cheap measures\n        return max(jaccard, ratio)\n\n    async def save_tool_usage(\n        self,\n        question: str,\n        tool_name: str,\n        args: Dict[str, Any],\n        context: ToolContext,\n        success: bool = True,\n        metadata: Optional[Dict[str, Any]] = None,\n    ) -> None:\n        \"\"\"Save a tool usage pattern for future reference.\"\"\"\n        tm = ToolMemory(\n            memory_id=str(uuid.uuid4()),\n            question=question,\n            tool_name=tool_name,\n            args=args,\n            timestamp=self._now_iso(),\n            success=success,\n            metadata=metadata or {},\n        )\n        async with self._lock:\n            self._memories.append(tm)\n            # Optional FIFO eviction\n            if len(self._memories) > self._max_items:\n                overflow = len(self._memories) - self._max_items\n                del self._memories[:overflow]\n\n    async def save_text_memory(self, content: str, context: ToolContext) -> TextMemory:\n        \"\"\"Store a text memory in RAM.\"\"\"\n        tm = TextMemory(\n            memory_id=str(uuid.uuid4()), content=content, timestamp=self._now_iso()\n        )\n        async with self._lock:\n            self._text_memories.append(tm)\n            if len(self._text_memories) > self._max_items:\n                overflow = len(self._text_memories) - self._max_items\n                del self._text_memories[:overflow]\n        return tm\n\n    async def search_similar_usage(\n        self,\n        question: str,\n        context: ToolContext,\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n        tool_name_filter: Optional[str] = None,\n    ) -> List[ToolMemorySearchResult]:\n        \"\"\"Search for similar tool usage patterns based on a question.\"\"\"\n        q = self._normalize(question)\n\n        async with self._lock:\n            # Filter candidates by tool name and success status\n            candidates = [\n                m\n                for m in self._memories\n                if m.success\n                and (tool_name_filter is None or m.tool_name == tool_name_filter)\n            ]\n\n            # Score each candidate by question similarity\n            results: List[tuple[ToolMemory, float]] = []\n            for m in candidates:\n                score = self._similarity(q, m.question)\n                results.append((m, min(score, 1.0)))\n\n            # Filter by threshold and sort by score\n            results = [(m, s) for (m, s) in results if s >= similarity_threshold]\n            results.sort(key=lambda x: x[1], reverse=True)\n\n            # Build ranked response\n            out: List[ToolMemorySearchResult] = []\n            for idx, (m, s) in enumerate(results[:limit], start=1):\n                out.append(\n                    ToolMemorySearchResult(memory=m, similarity_score=s, rank=idx)\n                )\n            return out\n\n    async def search_text_memories(\n        self,\n        query: str,\n        context: ToolContext,\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n    ) -> List[TextMemorySearchResult]:\n        \"\"\"Search free-form text memories using the demo similarity metric.\"\"\"\n        normalized_query = self._normalize(query)\n\n        async with self._lock:\n            scored: List[tuple[TextMemory, float]] = []\n            for memory in self._text_memories:\n                score = self._similarity(normalized_query, memory.content)\n                scored.append((memory, min(score, 1.0)))\n\n            scored = [\n                (memory, score)\n                for memory, score in scored\n                if score >= similarity_threshold\n            ]\n            scored.sort(key=lambda item: item[1], reverse=True)\n\n            results: List[TextMemorySearchResult] = []\n            for idx, (memory, score) in enumerate(scored[:limit], start=1):\n                results.append(\n                    TextMemorySearchResult(\n                        memory=memory, similarity_score=score, rank=idx\n                    )\n                )\n            return results\n\n    async def get_recent_memories(\n        self, context: ToolContext, limit: int = 10\n    ) -> List[ToolMemory]:\n        \"\"\"Get recently added memories. Returns most recent memories first.\"\"\"\n        async with self._lock:\n            # Return memories in reverse order (most recent first)\n            return list(reversed(self._memories[-limit:]))\n\n    async def get_recent_text_memories(\n        self, context: ToolContext, limit: int = 10\n    ) -> List[TextMemory]:\n        \"\"\"Return recently added text memories.\"\"\"\n        async with self._lock:\n            return list(reversed(self._text_memories[-limit:]))\n\n    async def delete_text_memory(self, context: ToolContext, memory_id: str) -> bool:\n        \"\"\"Delete a stored text memory by ID.\"\"\"\n        async with self._lock:\n            for index, memory in enumerate(self._text_memories):\n                if memory.memory_id == memory_id:\n                    del self._text_memories[index]\n                    return True\n            return False\n\n    async def delete_by_id(self, context: ToolContext, memory_id: str) -> bool:\n        \"\"\"Delete a memory by its ID. Returns True if deleted, False if not found.\"\"\"\n        async with self._lock:\n            for i, m in enumerate(self._memories):\n                if m.memory_id == memory_id:\n                    del self._memories[i]\n                    return True\n            return False\n\n    async def clear_memories(\n        self,\n        context: ToolContext,\n        tool_name: Optional[str] = None,\n        before_date: Optional[str] = None,\n    ) -> int:\n        \"\"\"Clear stored memories. Returns number of memories deleted.\"\"\"\n        async with self._lock:\n            original_tool_count = len(self._memories)\n            original_text_count = len(self._text_memories)\n\n            # Filter memories to keep\n            kept_memories = []\n            for m in self._memories:\n                should_delete = True\n\n                # Check tool name filter\n                if tool_name and m.tool_name != tool_name:\n                    should_delete = False\n\n                # Check date filter\n                if should_delete and before_date and m.timestamp:\n                    if m.timestamp >= before_date:\n                        should_delete = False\n\n                # If no filters specified, delete all\n                if tool_name is None and before_date is None:\n                    should_delete = True\n\n                # Keep if should not delete\n                if not should_delete:\n                    kept_memories.append(m)\n\n            self._memories = kept_memories\n            deleted_tool_count = original_tool_count - len(self._memories)\n\n            # Apply filters to text memories (tool filter ignored)\n            kept_text_memories = []\n            for memory in self._text_memories:\n                should_delete = (\n                    tool_name is None\n                )  # only delete text when not targeting a tool\n\n                if before_date and memory.timestamp:\n                    if memory.timestamp >= before_date:\n                        should_delete = False\n\n                if not should_delete:\n                    kept_text_memories.append(memory)\n\n            self._text_memories = kept_text_memories\n            deleted_text_count = original_text_count - len(self._text_memories)\n\n            return deleted_tool_count + deleted_text_count\n"
  },
  {
    "path": "src/vanna/integrations/local/audit.py",
    "content": "\"\"\"\nLocal audit logger implementation using Python logging.\n\nThis module provides a simple audit logger that writes events using\nthe standard Python logging module, useful for development and testing.\n\"\"\"\n\nimport json\nimport logging\nfrom typing import Optional\n\nfrom vanna.core.audit import AuditEvent, AuditLogger\n\nlogger = logging.getLogger(__name__)\n\n\nclass LoggingAuditLogger(AuditLogger):\n    \"\"\"Audit logger that writes events to Python logger as structured JSON.\n\n    This implementation uses logger.info() to emit audit events as JSON,\n    making them easy to parse and route to log aggregation systems.\n\n    Example:\n        audit_logger = LoggingAuditLogger()\n        agent = Agent(\n            llm_service=...,\n            audit_logger=audit_logger\n        )\n    \"\"\"\n\n    def __init__(self, log_level: int = logging.INFO):\n        \"\"\"Initialize the logging audit logger.\n\n        Args:\n            log_level: Log level to use for audit events (default: INFO)\n        \"\"\"\n        self.log_level = log_level\n\n    async def log_event(self, event: AuditEvent) -> None:\n        \"\"\"Log an audit event as structured JSON.\n\n        Args:\n            event: The audit event to log\n        \"\"\"\n        try:\n            # Convert event to dict for JSON serialization\n            event_dict = event.model_dump(mode=\"json\", exclude_none=True)\n\n            # Format as single-line JSON for easy parsing\n            event_json = json.dumps(event_dict, separators=(\",\", \":\"))\n\n            # Log with structured prefix for easy filtering\n            logger.log(\n                self.log_level,\n                f\"[AUDIT] {event.event_type.value} | {event_json}\",\n            )\n        except Exception as e:\n            # Don't fail the operation if audit logging fails\n            logger.error(f\"Failed to log audit event: {e}\", exc_info=True)\n"
  },
  {
    "path": "src/vanna/integrations/local/file_system.py",
    "content": "\"\"\"\nLocal file system implementation.\n\nThis module provides a local file system implementation with per-user isolation.\n\"\"\"\n\nimport asyncio\nimport hashlib\nfrom pathlib import Path\nfrom typing import List, Optional\n\nfrom vanna.capabilities.file_system import CommandResult, FileSearchMatch, FileSystem\nfrom vanna.core.tool import ToolContext\n\nMAX_SEARCH_FILE_BYTES = 1_000_000\n\n\nclass LocalFileSystem(FileSystem):\n    \"\"\"Local file system implementation with per-user isolation.\"\"\"\n\n    def __init__(self, working_directory: str = \".\"):\n        \"\"\"Initialize with a working directory.\n\n        Args:\n            working_directory: Base directory where user-specific folders will be created\n        \"\"\"\n        self.working_directory = Path(working_directory)\n\n    def _get_user_directory(self, context: ToolContext) -> Path:\n        \"\"\"Get the user-specific directory by hashing the user ID.\n\n        Args:\n            context: Tool context containing user information\n\n        Returns:\n            Path to the user-specific directory\n        \"\"\"\n        # Hash the user ID to create a directory name\n        user_hash = hashlib.sha256(context.user.id.encode()).hexdigest()[:16]\n        user_dir = self.working_directory / user_hash\n\n        # Create the directory if it doesn't exist\n        user_dir.mkdir(parents=True, exist_ok=True)\n\n        return user_dir\n\n    def _resolve_path(self, path: str, context: ToolContext) -> Path:\n        \"\"\"Resolve a path relative to the user's directory.\n\n        Args:\n            path: Path relative to user directory\n            context: Tool context containing user information\n\n        Returns:\n            Absolute path within user's directory\n        \"\"\"\n        user_dir = self._get_user_directory(context)\n        resolved = user_dir / path\n\n        # Ensure the path is within the user's directory (prevent directory traversal)\n        try:\n            resolved.resolve().relative_to(user_dir.resolve())\n        except ValueError:\n            raise PermissionError(\n                f\"Access denied: path '{path}' is outside user directory\"\n            )\n\n        return resolved\n\n    async def list_files(self, directory: str, context: ToolContext) -> List[str]:\n        \"\"\"List files in a directory within the user's isolated space.\"\"\"\n        directory_path = self._resolve_path(directory, context)\n\n        if not directory_path.exists():\n            raise FileNotFoundError(f\"Directory '{directory}' does not exist\")\n\n        if not directory_path.is_dir():\n            raise NotADirectoryError(f\"'{directory}' is not a directory\")\n\n        files = []\n        for item in directory_path.iterdir():\n            if item.is_file():\n                files.append(item.name)\n\n        return sorted(files)\n\n    async def read_file(self, filename: str, context: ToolContext) -> str:\n        \"\"\"Read the contents of a file within the user's isolated space.\"\"\"\n        file_path = self._resolve_path(filename, context)\n\n        if not file_path.exists():\n            raise FileNotFoundError(f\"File '{filename}' does not exist\")\n\n        if not file_path.is_file():\n            raise IsADirectoryError(f\"'{filename}' is a directory, not a file\")\n\n        return file_path.read_text(encoding=\"utf-8\")\n\n    async def write_file(\n        self, filename: str, content: str, context: ToolContext, overwrite: bool = False\n    ) -> None:\n        \"\"\"Write content to a file within the user's isolated space.\"\"\"\n        file_path = self._resolve_path(filename, context)\n\n        # Create parent directories if they don't exist\n        file_path.parent.mkdir(parents=True, exist_ok=True)\n\n        if file_path.exists() and not overwrite:\n            raise FileExistsError(\n                f\"File '{filename}' already exists. Use overwrite=True to replace it.\"\n            )\n\n        file_path.write_text(content, encoding=\"utf-8\")\n\n    async def exists(self, path: str, context: ToolContext) -> bool:\n        \"\"\"Check if a file or directory exists within the user's isolated space.\"\"\"\n        try:\n            resolved_path = self._resolve_path(path, context)\n            return resolved_path.exists()\n        except PermissionError:\n            return False\n\n    async def is_directory(self, path: str, context: ToolContext) -> bool:\n        \"\"\"Check if a path is a directory within the user's isolated space.\"\"\"\n        try:\n            resolved_path = self._resolve_path(path, context)\n            return resolved_path.exists() and resolved_path.is_dir()\n        except PermissionError:\n            return False\n\n    async def search_files(\n        self,\n        query: str,\n        context: ToolContext,\n        *,\n        max_results: int = 20,\n        include_content: bool = False,\n    ) -> List[FileSearchMatch]:\n        \"\"\"Search for files within the user's isolated space.\"\"\"\n\n        trimmed_query = query.strip()\n        if not trimmed_query:\n            raise ValueError(\"Search query must not be empty\")\n\n        user_dir = self._get_user_directory(context)\n        matches: List[FileSearchMatch] = []\n        query_lower = trimmed_query.lower()\n\n        for path in user_dir.rglob(\"*\"):\n            if len(matches) >= max_results:\n                break\n\n            if not path.is_file():\n                continue\n\n            relative_path = path.relative_to(user_dir).as_posix()\n            include_entry = False\n            snippet: Optional[str] = None\n\n            if query_lower in path.name.lower():\n                include_entry = True\n                snippet = \"[filename match]\"\n\n            content: Optional[str] = None\n            if include_content:\n                try:\n                    size = path.stat().st_size\n                except OSError:\n                    if include_entry:\n                        matches.append(\n                            FileSearchMatch(path=relative_path, snippet=snippet)\n                        )\n                    continue\n\n                if size <= MAX_SEARCH_FILE_BYTES:\n                    try:\n                        content = path.read_text(encoding=\"utf-8\")\n                    except (UnicodeDecodeError, OSError):\n                        content = None\n                elif not include_entry:\n                    # Skip oversized files if they do not match by name\n                    continue\n\n            if include_content and content is not None:\n                if query_lower in content.lower():\n                    # Create snippet\n                    lowered = content.lower()\n                    index = lowered.find(query_lower)\n                    if index != -1:\n                        context_window = 60\n                        start = max(0, index - context_window)\n                        end = min(len(content), index + len(query) + context_window)\n                        snippet = content[start:end].replace(\"\\n\", \" \").strip()\n                        if start > 0:\n                            snippet = f\"…{snippet}\"\n                        if end < len(content):\n                            snippet = f\"{snippet}…\"\n                    include_entry = True\n                elif not include_entry:\n                    continue\n\n            if include_entry:\n                matches.append(FileSearchMatch(path=relative_path, snippet=snippet))\n\n        return matches\n\n    async def run_bash(\n        self,\n        command: str,\n        context: ToolContext,\n        *,\n        timeout: Optional[float] = None,\n    ) -> CommandResult:\n        \"\"\"Execute a bash command within the user's isolated space.\"\"\"\n\n        if not command.strip():\n            raise ValueError(\"Command must not be empty\")\n\n        user_dir = self._get_user_directory(context)\n\n        process = await asyncio.create_subprocess_shell(\n            command,\n            stdout=asyncio.subprocess.PIPE,\n            stderr=asyncio.subprocess.PIPE,\n            cwd=str(user_dir),\n        )\n\n        try:\n            stdout_bytes, stderr_bytes = await asyncio.wait_for(\n                process.communicate(), timeout=timeout\n            )\n        except asyncio.TimeoutError as exc:\n            process.kill()\n            await process.wait()\n            raise TimeoutError(f\"Command timed out after {timeout} seconds\") from exc\n\n        stdout = stdout_bytes.decode(\"utf-8\", errors=\"replace\")\n        stderr = stderr_bytes.decode(\"utf-8\", errors=\"replace\")\n\n        return CommandResult(\n            stdout=stdout, stderr=stderr, returncode=process.returncode or 0\n        )\n"
  },
  {
    "path": "src/vanna/integrations/local/file_system_conversation_store.py",
    "content": "\"\"\"\nFile system conversation store implementation.\n\nThis module provides a file-based implementation of the ConversationStore\ninterface that persists conversations to disk as a directory structure.\n\"\"\"\n\nimport json\nimport os\nfrom pathlib import Path\nfrom typing import Dict, List, Optional\nfrom datetime import datetime\nimport time\n\nfrom vanna.core.storage import ConversationStore, Conversation, Message\nfrom vanna.core.user import User\n\n\nclass FileSystemConversationStore(ConversationStore):\n    \"\"\"File system-based conversation store.\n\n    Stores conversations as directories with individual message files:\n    conversations/{conversation_id}/\n        metadata.json - conversation metadata (id, user info, timestamps)\n        messages/\n            {timestamp}_{index}.json - individual message files\n    \"\"\"\n\n    def __init__(self, base_dir: str = \"conversations\") -> None:\n        \"\"\"Initialize the file system conversation store.\n\n        Args:\n            base_dir: Base directory for storing conversations\n        \"\"\"\n        self.base_dir = Path(base_dir)\n        self.base_dir.mkdir(parents=True, exist_ok=True)\n\n    def _get_conversation_dir(self, conversation_id: str) -> Path:\n        \"\"\"Get the directory path for a conversation.\"\"\"\n        return self.base_dir / conversation_id\n\n    def _get_metadata_path(self, conversation_id: str) -> Path:\n        \"\"\"Get the metadata file path for a conversation.\"\"\"\n        return self._get_conversation_dir(conversation_id) / \"metadata.json\"\n\n    def _get_messages_dir(self, conversation_id: str) -> Path:\n        \"\"\"Get the messages directory for a conversation.\"\"\"\n        return self._get_conversation_dir(conversation_id) / \"messages\"\n\n    def _save_metadata(self, conversation: Conversation) -> None:\n        \"\"\"Save conversation metadata to disk.\"\"\"\n        conv_dir = self._get_conversation_dir(conversation.id)\n        conv_dir.mkdir(parents=True, exist_ok=True)\n\n        metadata = {\n            \"id\": conversation.id,\n            \"user\": conversation.user.model_dump(mode=\"json\"),\n            \"created_at\": conversation.created_at.isoformat(),\n            \"updated_at\": conversation.updated_at.isoformat(),\n        }\n\n        metadata_path = self._get_metadata_path(conversation.id)\n        with open(metadata_path, \"w\") as f:\n            json.dump(metadata, f, indent=2)\n\n    def _load_messages(self, conversation_id: str) -> List[Message]:\n        \"\"\"Load all messages for a conversation.\"\"\"\n        messages_dir = self._get_messages_dir(conversation_id)\n\n        if not messages_dir.exists():\n            return []\n\n        messages = []\n        # Sort message files by name (timestamp_index ensures correct order)\n        message_files = sorted(messages_dir.glob(\"*.json\"))\n\n        for file_path in message_files:\n            try:\n                with open(file_path, \"r\") as f:\n                    data = json.load(f)\n                message = Message.model_validate(data)\n                messages.append(message)\n            except (json.JSONDecodeError, ValueError) as e:\n                print(f\"Failed to load message from {file_path}: {e}\")\n                continue\n\n        return messages\n\n    def _append_message(\n        self, conversation_id: str, message: Message, index: int\n    ) -> None:\n        \"\"\"Append a message to the conversation.\"\"\"\n        messages_dir = self._get_messages_dir(conversation_id)\n        messages_dir.mkdir(parents=True, exist_ok=True)\n\n        # Use timestamp + index to ensure unique, ordered filenames\n        timestamp = int(time.time() * 1000000)  # microseconds\n        filename = f\"{timestamp}_{index:06d}.json\"\n        file_path = messages_dir / filename\n\n        with open(file_path, \"w\") as f:\n            json.dump(message.model_dump(mode=\"json\"), f, indent=2)\n\n    async def create_conversation(\n        self, conversation_id: str, user: User, initial_message: str\n    ) -> Conversation:\n        \"\"\"Create a new conversation with the specified ID.\"\"\"\n        conversation = Conversation(\n            id=conversation_id,\n            user=user,\n            messages=[Message(role=\"user\", content=initial_message)],\n        )\n\n        # Save metadata\n        self._save_metadata(conversation)\n\n        # Save initial message\n        self._append_message(conversation_id, conversation.messages[0], 0)\n\n        return conversation\n\n    async def get_conversation(\n        self, conversation_id: str, user: User\n    ) -> Optional[Conversation]:\n        \"\"\"Get conversation by ID, scoped to user.\"\"\"\n        metadata_path = self._get_metadata_path(conversation_id)\n\n        if not metadata_path.exists():\n            return None\n\n        try:\n            # Load metadata\n            with open(metadata_path, \"r\") as f:\n                metadata = json.load(f)\n\n            # Verify ownership\n            if metadata[\"user\"][\"id\"] != user.id:\n                return None\n\n            # Load all messages\n            messages = self._load_messages(conversation_id)\n\n            # Reconstruct conversation\n            conversation = Conversation(\n                id=metadata[\"id\"],\n                user=User.model_validate(metadata[\"user\"]),\n                messages=messages,\n                created_at=datetime.fromisoformat(metadata[\"created_at\"]),\n                updated_at=datetime.fromisoformat(metadata[\"updated_at\"]),\n            )\n\n            return conversation\n        except (json.JSONDecodeError, ValueError, KeyError) as e:\n            print(f\"Failed to load conversation {conversation_id}: {e}\")\n            return None\n\n    async def update_conversation(self, conversation: Conversation) -> None:\n        \"\"\"Update conversation with new messages.\"\"\"\n        # Update the updated_at timestamp\n        conversation.updated_at = datetime.now()\n\n        # Save updated metadata\n        self._save_metadata(conversation)\n\n        # Get existing messages count to determine new message indices\n        existing_messages = self._load_messages(conversation.id)\n        existing_count = len(existing_messages)\n\n        # Only append new messages (ones not already saved)\n        for i, message in enumerate(\n            conversation.messages[existing_count:], start=existing_count\n        ):\n            self._append_message(conversation.id, message, i)\n\n    async def delete_conversation(self, conversation_id: str, user: User) -> bool:\n        \"\"\"Delete conversation.\"\"\"\n        conv_dir = self._get_conversation_dir(conversation_id)\n\n        if not conv_dir.exists():\n            return False\n\n        # Verify ownership before deleting\n        conversation = await self.get_conversation(conversation_id, user)\n        if not conversation:\n            return False\n\n        try:\n            # Delete all message files\n            messages_dir = self._get_messages_dir(conversation_id)\n            if messages_dir.exists():\n                for file_path in messages_dir.glob(\"*.json\"):\n                    file_path.unlink()\n                messages_dir.rmdir()\n\n            # Delete metadata\n            metadata_path = self._get_metadata_path(conversation_id)\n            if metadata_path.exists():\n                metadata_path.unlink()\n\n            # Delete conversation directory\n            conv_dir.rmdir()\n\n            return True\n        except OSError as e:\n            print(f\"Failed to delete conversation {conversation_id}: {e}\")\n            return False\n\n    async def list_conversations(\n        self, user: User, limit: int = 50, offset: int = 0\n    ) -> List[Conversation]:\n        \"\"\"List conversations for user.\"\"\"\n        if not self.base_dir.exists():\n            return []\n\n        conversations = []\n\n        # Iterate through all conversation directories\n        for conv_dir in self.base_dir.iterdir():\n            if not conv_dir.is_dir():\n                continue\n\n            metadata_path = conv_dir / \"metadata.json\"\n            if not metadata_path.exists():\n                continue\n\n            try:\n                # Load metadata\n                with open(metadata_path, \"r\") as f:\n                    metadata = json.load(f)\n\n                # Skip conversations not owned by this user\n                if metadata[\"user\"][\"id\"] != user.id:\n                    continue\n\n                # Load messages\n                messages = self._load_messages(conv_dir.name)\n\n                # Reconstruct conversation\n                conversation = Conversation(\n                    id=metadata[\"id\"],\n                    user=User.model_validate(metadata[\"user\"]),\n                    messages=messages,\n                    created_at=datetime.fromisoformat(metadata[\"created_at\"]),\n                    updated_at=datetime.fromisoformat(metadata[\"updated_at\"]),\n                )\n                conversations.append(conversation)\n            except (json.JSONDecodeError, ValueError, KeyError) as e:\n                print(f\"Failed to load conversation from {conv_dir}: {e}\")\n                continue\n\n        # Sort by updated_at desc\n        conversations.sort(key=lambda x: x.updated_at, reverse=True)\n\n        # Apply pagination\n        return conversations[offset : offset + limit]\n"
  },
  {
    "path": "src/vanna/integrations/local/storage.py",
    "content": "\"\"\"\nIn-memory conversation store implementation.\n\nThis module provides a simple in-memory implementation of the ConversationStore\ninterface, useful for testing and development.\n\"\"\"\n\nfrom typing import Dict, List, Optional\n\nfrom vanna.core.storage import ConversationStore, Conversation, Message\nfrom vanna.core.user import User\n\n\nclass MemoryConversationStore(ConversationStore):\n    \"\"\"In-memory conversation store.\"\"\"\n\n    def __init__(self) -> None:\n        self._conversations: Dict[str, Conversation] = {}\n\n    async def create_conversation(\n        self, conversation_id: str, user: User, initial_message: str\n    ) -> Conversation:\n        \"\"\"Create a new conversation with the specified ID.\"\"\"\n        conversation = Conversation(\n            id=conversation_id,\n            user=user,\n            messages=[Message(role=\"user\", content=initial_message)],\n        )\n        self._conversations[conversation_id] = conversation\n        return conversation\n\n    async def get_conversation(\n        self, conversation_id: str, user: User\n    ) -> Optional[Conversation]:\n        \"\"\"Get conversation by ID, scoped to user.\"\"\"\n        conversation = self._conversations.get(conversation_id)\n        if conversation and conversation.user.id == user.id:\n            return conversation\n        return None\n\n    async def update_conversation(self, conversation: Conversation) -> None:\n        \"\"\"Update conversation with new messages.\"\"\"\n        self._conversations[conversation.id] = conversation\n\n    async def delete_conversation(self, conversation_id: str, user: User) -> bool:\n        \"\"\"Delete conversation.\"\"\"\n        conversation = await self.get_conversation(conversation_id, user)\n        if conversation:\n            del self._conversations[conversation_id]\n            return True\n        return False\n\n    async def list_conversations(\n        self, user: User, limit: int = 50, offset: int = 0\n    ) -> List[Conversation]:\n        \"\"\"List conversations for user.\"\"\"\n        user_conversations = [\n            conv for conv in self._conversations.values() if conv.user.id == user.id\n        ]\n        # Sort by updated_at desc\n        user_conversations.sort(key=lambda x: x.updated_at, reverse=True)\n        return user_conversations[offset : offset + limit]\n"
  },
  {
    "path": "src/vanna/integrations/marqo/__init__.py",
    "content": "\"\"\"\nMarqo integration for Vanna Agents.\n\"\"\"\n\nfrom .agent_memory import MarqoAgentMemory\n\n__all__ = [\"MarqoAgentMemory\"]\n"
  },
  {
    "path": "src/vanna/integrations/marqo/agent_memory.py",
    "content": "\"\"\"\nMarqo vector database implementation of AgentMemory.\n\nThis implementation uses Marqo for vector storage of tool usage patterns.\n\"\"\"\n\nimport json\nimport uuid\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional\nimport asyncio\nfrom concurrent.futures import ThreadPoolExecutor\n\ntry:\n    import marqo\n\n    MARQO_AVAILABLE = True\nexcept ImportError:\n    MARQO_AVAILABLE = False\n\nfrom vanna.capabilities.agent_memory import (\n    AgentMemory,\n    TextMemory,\n    TextMemorySearchResult,\n    ToolMemory,\n    ToolMemorySearchResult,\n)\nfrom vanna.core.tool import ToolContext\n\n\nclass MarqoAgentMemory(AgentMemory):\n    \"\"\"Marqo-based implementation of AgentMemory.\"\"\"\n\n    def __init__(\n        self,\n        url: str = \"http://localhost:8882\",\n        index_name: str = \"tool-memories\",\n        api_key: Optional[str] = None,\n    ):\n        if not MARQO_AVAILABLE:\n            raise ImportError(\n                \"Marqo is required for MarqoAgentMemory. Install with: pip install marqo\"\n            )\n\n        self.url = url\n        self.index_name = index_name\n        self.api_key = api_key\n        self._client = None\n        self._executor = ThreadPoolExecutor(max_workers=2)\n\n    def _get_client(self):\n        \"\"\"Get or create Marqo client.\"\"\"\n        if self._client is None:\n            self._client = marqo.Client(url=self.url, api_key=self.api_key)\n\n            # Create index if it doesn't exist\n            try:\n                self._client.get_index(self.index_name)\n            except Exception:\n                self._client.create_index(self.index_name)\n\n        return self._client\n\n    async def save_tool_usage(\n        self,\n        question: str,\n        tool_name: str,\n        args: Dict[str, Any],\n        context: ToolContext,\n        success: bool = True,\n        metadata: Optional[Dict[str, Any]] = None,\n    ) -> None:\n        \"\"\"Save a tool usage pattern.\"\"\"\n\n        def _save():\n            client = self._get_client()\n\n            memory_id = str(uuid.uuid4())\n            timestamp = datetime.now().isoformat()\n\n            document = {\n                \"_id\": memory_id,\n                \"question\": question,\n                \"tool_name\": tool_name,\n                \"args\": json.dumps(args),\n                \"timestamp\": timestamp,\n                \"success\": success,\n                \"metadata\": json.dumps(metadata or {}),\n            }\n\n            client.index(self.index_name).add_documents(\n                [document], tensor_fields=[\"question\"]\n            )\n\n        await asyncio.get_event_loop().run_in_executor(self._executor, _save)\n\n    async def search_similar_usage(\n        self,\n        question: str,\n        context: ToolContext,\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n        tool_name_filter: Optional[str] = None,\n    ) -> List[ToolMemorySearchResult]:\n        \"\"\"Search for similar tool usage patterns.\"\"\"\n\n        def _search():\n            client = self._get_client()\n\n            # Build filter\n            filter_string = \"success:true\"\n            if tool_name_filter:\n                filter_string += f\" AND tool_name:{tool_name_filter}\"\n\n            results = client.index(self.index_name).search(\n                q=question, limit=limit, filter_string=filter_string\n            )\n\n            search_results = []\n            for i, hit in enumerate(results[\"hits\"]):\n                # Marqo returns score\n                similarity_score = hit.get(\"_score\", 0)\n\n                if similarity_score >= similarity_threshold:\n                    args = json.loads(hit.get(\"args\", \"{}\"))\n                    metadata_dict = json.loads(hit.get(\"metadata\", \"{}\"))\n\n                    memory = ToolMemory(\n                        memory_id=hit[\"_id\"],\n                        question=hit[\"question\"],\n                        tool_name=hit[\"tool_name\"],\n                        args=args,\n                        timestamp=hit.get(\"timestamp\"),\n                        success=hit.get(\"success\", True),\n                        metadata=metadata_dict,\n                    )\n\n                    search_results.append(\n                        ToolMemorySearchResult(\n                            memory=memory, similarity_score=similarity_score, rank=i + 1\n                        )\n                    )\n\n            return search_results\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _search)\n\n    async def get_recent_memories(\n        self, context: ToolContext, limit: int = 10\n    ) -> List[ToolMemory]:\n        \"\"\"Get recently added memories.\"\"\"\n\n        def _get_recent():\n            client = self._get_client()\n\n            # Search with wildcard and sort by timestamp\n            results = client.index(self.index_name).search(\n                q=\"*\", limit=limit, sort=\"timestamp:desc\"\n            )\n\n            memories = []\n            for hit in results.get(\"hits\", []):\n                args = json.loads(hit.get(\"args\", \"{}\"))\n                metadata_dict = json.loads(hit.get(\"metadata\", \"{}\"))\n\n                memory = ToolMemory(\n                    memory_id=hit[\"_id\"],\n                    question=hit[\"question\"],\n                    tool_name=hit[\"tool_name\"],\n                    args=args,\n                    timestamp=hit.get(\"timestamp\"),\n                    success=hit.get(\"success\", True),\n                    metadata=metadata_dict,\n                )\n                memories.append(memory)\n\n            return memories\n\n        return await asyncio.get_event_loop().run_in_executor(\n            self._executor, _get_recent\n        )\n\n    async def delete_by_id(self, context: ToolContext, memory_id: str) -> bool:\n        \"\"\"Delete a memory by its ID.\"\"\"\n\n        def _delete():\n            client = self._get_client()\n\n            try:\n                client.index(self.index_name).delete_documents(ids=[memory_id])\n                return True\n            except Exception:\n                return False\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _delete)\n\n    async def save_text_memory(self, content: str, context: ToolContext) -> TextMemory:\n        \"\"\"Save a text memory.\"\"\"\n\n        def _save():\n            client = self._get_client()\n\n            memory_id = str(uuid.uuid4())\n            timestamp = datetime.now().isoformat()\n\n            document = {\n                \"_id\": memory_id,\n                \"content\": content,\n                \"timestamp\": timestamp,\n                \"is_text_memory\": True,\n            }\n\n            client.index(self.index_name).add_documents(\n                [document], tensor_fields=[\"content\"]\n            )\n\n            return TextMemory(memory_id=memory_id, content=content, timestamp=timestamp)\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _save)\n\n    async def search_text_memories(\n        self,\n        query: str,\n        context: ToolContext,\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n    ) -> List[TextMemorySearchResult]:\n        \"\"\"Search for similar text memories.\"\"\"\n\n        def _search():\n            client = self._get_client()\n\n            filter_string = \"is_text_memory:true\"\n\n            results = client.index(self.index_name).search(\n                q=query, limit=limit, filter_string=filter_string\n            )\n\n            search_results = []\n            for i, hit in enumerate(results[\"hits\"]):\n                similarity_score = hit.get(\"_score\", 0)\n\n                if similarity_score >= similarity_threshold:\n                    memory = TextMemory(\n                        memory_id=hit[\"_id\"],\n                        content=hit.get(\"content\", \"\"),\n                        timestamp=hit.get(\"timestamp\"),\n                    )\n\n                    search_results.append(\n                        TextMemorySearchResult(\n                            memory=memory, similarity_score=similarity_score, rank=i + 1\n                        )\n                    )\n\n            return search_results\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _search)\n\n    async def get_recent_text_memories(\n        self, context: ToolContext, limit: int = 10\n    ) -> List[TextMemory]:\n        \"\"\"Get recently added text memories.\"\"\"\n\n        def _get_recent():\n            client = self._get_client()\n\n            results = client.index(self.index_name).search(\n                q=\"*\",\n                limit=limit,\n                filter_string=\"is_text_memory:true\",\n                sort=\"timestamp:desc\",\n            )\n\n            memories = []\n            for hit in results.get(\"hits\", []):\n                memory = TextMemory(\n                    memory_id=hit[\"_id\"],\n                    content=hit.get(\"content\", \"\"),\n                    timestamp=hit.get(\"timestamp\"),\n                )\n                memories.append(memory)\n\n            return memories\n\n        return await asyncio.get_event_loop().run_in_executor(\n            self._executor, _get_recent\n        )\n\n    async def delete_text_memory(self, context: ToolContext, memory_id: str) -> bool:\n        \"\"\"Delete a text memory by its ID.\"\"\"\n\n        def _delete():\n            client = self._get_client()\n\n            try:\n                client.index(self.index_name).delete_documents(ids=[memory_id])\n                return True\n            except Exception:\n                return False\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _delete)\n\n    async def clear_memories(\n        self,\n        context: ToolContext,\n        tool_name: Optional[str] = None,\n        before_date: Optional[str] = None,\n    ) -> int:\n        \"\"\"Clear stored memories.\"\"\"\n\n        def _clear():\n            client = self._get_client()\n\n            # Build filter for search\n            filter_parts = []\n            if tool_name:\n                filter_parts.append(f\"tool_name:{tool_name}\")\n            if before_date:\n                filter_parts.append(f\"timestamp:[* TO {before_date}]\")\n\n            if filter_parts or (tool_name is None and before_date is None):\n                filter_string = \" AND \".join(filter_parts) if filter_parts else None\n\n                if filter_string:\n                    # Search for documents to delete\n                    results = client.index(self.index_name).search(\n                        q=\"*\",\n                        limit=1000,  # Max results\n                        filter_string=filter_string,\n                    )\n\n                    ids_to_delete = [hit[\"_id\"] for hit in results.get(\"hits\", [])]\n\n                    if ids_to_delete:\n                        client.index(self.index_name).delete_documents(\n                            ids=ids_to_delete\n                        )\n\n                    return len(ids_to_delete)\n                else:\n                    # Delete entire index and recreate\n                    try:\n                        client.delete_index(self.index_name)\n                        client.create_index(self.index_name)\n                    except Exception:\n                        pass\n                    return 0\n\n            return 0\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _clear)\n"
  },
  {
    "path": "src/vanna/integrations/milvus/__init__.py",
    "content": "\"\"\"\nMilvus integration for Vanna Agents.\n\"\"\"\n\nfrom .agent_memory import MilvusAgentMemory\n\n__all__ = [\"MilvusAgentMemory\"]\n"
  },
  {
    "path": "src/vanna/integrations/milvus/agent_memory.py",
    "content": "\"\"\"\nMilvus vector database implementation of AgentMemory.\n\nThis implementation uses Milvus for distributed vector storage of tool usage patterns.\n\"\"\"\n\nimport json\nimport uuid\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional\nimport asyncio\nfrom concurrent.futures import ThreadPoolExecutor\n\ntry:\n    from pymilvus import (\n        connections,\n        Collection,\n        CollectionSchema,\n        FieldSchema,\n        DataType,\n        utility,\n    )\n\n    MILVUS_AVAILABLE = True\nexcept ImportError:\n    MILVUS_AVAILABLE = False\n\nfrom vanna.capabilities.agent_memory import (\n    AgentMemory,\n    TextMemory,\n    TextMemorySearchResult,\n    ToolMemory,\n    ToolMemorySearchResult,\n)\nfrom vanna.core.tool import ToolContext\n\n\nclass MilvusAgentMemory(AgentMemory):\n    \"\"\"Milvus-based implementation of AgentMemory.\"\"\"\n\n    def __init__(\n        self,\n        collection_name: str = \"tool_memories\",\n        host: str = \"localhost\",\n        port: int = 19530,\n        alias: str = \"default\",\n        dimension: int = 384,\n    ):\n        if not MILVUS_AVAILABLE:\n            raise ImportError(\n                \"Milvus is required for MilvusAgentMemory. Install with: pip install pymilvus\"\n            )\n\n        self.collection_name = collection_name\n        self.host = host\n        self.port = port\n        self.alias = alias\n        self.dimension = dimension\n        self._collection = None\n        self._executor = ThreadPoolExecutor(max_workers=2)\n\n    def _get_collection(self):\n        \"\"\"Get or create Milvus collection.\"\"\"\n        if self._collection is None:\n            # Connect to Milvus\n            connections.connect(alias=self.alias, host=self.host, port=self.port)\n\n            # Create collection if it doesn't exist\n            if not utility.has_collection(self.collection_name):\n                fields = [\n                    FieldSchema(\n                        name=\"id\",\n                        dtype=DataType.VARCHAR,\n                        is_primary=True,\n                        max_length=100,\n                    ),\n                    FieldSchema(\n                        name=\"embedding\",\n                        dtype=DataType.FLOAT_VECTOR,\n                        dim=self.dimension,\n                    ),\n                    FieldSchema(\n                        name=\"question\", dtype=DataType.VARCHAR, max_length=2000\n                    ),\n                    FieldSchema(\n                        name=\"tool_name\", dtype=DataType.VARCHAR, max_length=200\n                    ),\n                    FieldSchema(\n                        name=\"args_json\", dtype=DataType.VARCHAR, max_length=5000\n                    ),\n                    FieldSchema(\n                        name=\"timestamp\", dtype=DataType.VARCHAR, max_length=50\n                    ),\n                    FieldSchema(name=\"success\", dtype=DataType.BOOL),\n                    FieldSchema(\n                        name=\"metadata_json\", dtype=DataType.VARCHAR, max_length=5000\n                    ),\n                ]\n\n                schema = CollectionSchema(\n                    fields=fields, description=\"Tool usage memories\"\n                )\n                collection = Collection(name=self.collection_name, schema=schema)\n\n                # Create index for vector field\n                index_params = {\n                    \"index_type\": \"IVF_FLAT\",\n                    \"metric_type\": \"IP\",\n                    \"params\": {\"nlist\": 128},\n                }\n                collection.create_index(\n                    field_name=\"embedding\", index_params=index_params\n                )\n\n            self._collection = Collection(self.collection_name)\n            self._collection.load()\n\n        return self._collection\n\n    def _create_embedding(self, text: str) -> List[float]:\n        \"\"\"Create a simple embedding from text (placeholder).\"\"\"\n        import hashlib\n\n        hash_val = int(hashlib.md5(text.encode()).hexdigest(), 16)\n        return [(hash_val >> i) % 100 / 100.0 for i in range(self.dimension)]\n\n    async def save_tool_usage(\n        self,\n        question: str,\n        tool_name: str,\n        args: Dict[str, Any],\n        context: ToolContext,\n        success: bool = True,\n        metadata: Optional[Dict[str, Any]] = None,\n    ) -> None:\n        \"\"\"Save a tool usage pattern.\"\"\"\n\n        def _save():\n            collection = self._get_collection()\n\n            memory_id = str(uuid.uuid4())\n            timestamp = datetime.now().isoformat()\n            embedding = self._create_embedding(question)\n\n            entities = [\n                [memory_id],\n                [embedding],\n                [question],\n                [tool_name],\n                [json.dumps(args)],\n                [timestamp],\n                [success],\n                [json.dumps(metadata or {})],\n            ]\n\n            collection.insert(entities)\n            collection.flush()\n\n        await asyncio.get_event_loop().run_in_executor(self._executor, _save)\n\n    async def search_similar_usage(\n        self,\n        question: str,\n        context: ToolContext,\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n        tool_name_filter: Optional[str] = None,\n    ) -> List[ToolMemorySearchResult]:\n        \"\"\"Search for similar tool usage patterns.\"\"\"\n\n        def _search():\n            collection = self._get_collection()\n\n            embedding = self._create_embedding(question)\n\n            # Build filter expression\n            expr = \"success == true\"\n            if tool_name_filter:\n                expr += f' && tool_name == \"{tool_name_filter}\"'\n\n            search_params = {\"metric_type\": \"IP\", \"params\": {\"nprobe\": 10}}\n\n            results = collection.search(\n                data=[embedding],\n                anns_field=\"embedding\",\n                param=search_params,\n                limit=limit,\n                expr=expr,\n                output_fields=[\n                    \"id\",\n                    \"question\",\n                    \"tool_name\",\n                    \"args_json\",\n                    \"timestamp\",\n                    \"success\",\n                    \"metadata_json\",\n                ],\n            )\n\n            search_results = []\n            for i, hits in enumerate(results):\n                for j, hit in enumerate(hits):\n                    similarity_score = hit.distance\n\n                    if similarity_score >= similarity_threshold:\n                        args = json.loads(hit.entity.get(\"args_json\", \"{}\"))\n                        metadata_dict = json.loads(\n                            hit.entity.get(\"metadata_json\", \"{}\")\n                        )\n\n                        memory = ToolMemory(\n                            memory_id=hit.entity.get(\"id\"),\n                            question=hit.entity.get(\"question\"),\n                            tool_name=hit.entity.get(\"tool_name\"),\n                            args=args,\n                            timestamp=hit.entity.get(\"timestamp\"),\n                            success=hit.entity.get(\"success\", True),\n                            metadata=metadata_dict,\n                        )\n\n                        search_results.append(\n                            ToolMemorySearchResult(\n                                memory=memory,\n                                similarity_score=similarity_score,\n                                rank=j + 1,\n                            )\n                        )\n\n            return search_results\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _search)\n\n    async def get_recent_memories(\n        self, context: ToolContext, limit: int = 10\n    ) -> List[ToolMemory]:\n        \"\"\"Get recently added memories.\"\"\"\n\n        def _get_recent():\n            collection = self._get_collection()\n\n            # Query all entries and sort by timestamp\n            results = collection.query(\n                expr=\"id != ''\",\n                output_fields=[\n                    \"id\",\n                    \"question\",\n                    \"tool_name\",\n                    \"args_json\",\n                    \"timestamp\",\n                    \"success\",\n                    \"metadata_json\",\n                ],\n                limit=1000,\n            )\n\n            # Sort by timestamp\n            sorted_results = sorted(\n                results, key=lambda r: r.get(\"timestamp\", \"\"), reverse=True\n            )\n\n            memories = []\n            for result in sorted_results[:limit]:\n                args = json.loads(result.get(\"args_json\", \"{}\"))\n                metadata_dict = json.loads(result.get(\"metadata_json\", \"{}\"))\n\n                memory = ToolMemory(\n                    memory_id=result.get(\"id\"),\n                    question=result.get(\"question\"),\n                    tool_name=result.get(\"tool_name\"),\n                    args=args,\n                    timestamp=result.get(\"timestamp\"),\n                    success=result.get(\"success\", True),\n                    metadata=metadata_dict,\n                )\n                memories.append(memory)\n\n            return memories\n\n        return await asyncio.get_event_loop().run_in_executor(\n            self._executor, _get_recent\n        )\n\n    async def delete_by_id(self, context: ToolContext, memory_id: str) -> bool:\n        \"\"\"Delete a memory by its ID.\"\"\"\n\n        def _delete():\n            collection = self._get_collection()\n\n            try:\n                expr = f'id == \"{memory_id}\"'\n                collection.delete(expr)\n                return True\n            except Exception:\n                return False\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _delete)\n\n    async def save_text_memory(self, content: str, context: ToolContext) -> TextMemory:\n        \"\"\"Save a text memory.\"\"\"\n\n        def _save():\n            collection = self._get_collection()\n\n            memory_id = str(uuid.uuid4())\n            timestamp = datetime.now().isoformat()\n            embedding = self._create_embedding(content)\n\n            entities = [\n                [memory_id],\n                [embedding],\n                [content],\n                [\"\"],  # tool_name (empty for text memories)\n                [\"\"],  # args_json (empty for text memories)\n                [timestamp],\n                [True],  # success (always true for text memories)\n                [json.dumps({\"is_text_memory\": True})],  # metadata_json\n            ]\n\n            collection.insert(entities)\n            collection.flush()\n\n            return TextMemory(memory_id=memory_id, content=content, timestamp=timestamp)\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _save)\n\n    async def search_text_memories(\n        self,\n        query: str,\n        context: ToolContext,\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n    ) -> List[TextMemorySearchResult]:\n        \"\"\"Search for similar text memories.\"\"\"\n\n        def _search():\n            collection = self._get_collection()\n\n            embedding = self._create_embedding(query)\n\n            # Build filter expression for text memories\n            expr = 'tool_name == \"\"'\n\n            search_params = {\"metric_type\": \"IP\", \"params\": {\"nprobe\": 10}}\n\n            results = collection.search(\n                data=[embedding],\n                anns_field=\"embedding\",\n                param=search_params,\n                limit=limit,\n                expr=expr,\n                output_fields=[\"id\", \"question\", \"timestamp\", \"metadata_json\"],\n            )\n\n            search_results = []\n            for i, hits in enumerate(results):\n                for j, hit in enumerate(hits):\n                    similarity_score = hit.distance\n\n                    if similarity_score >= similarity_threshold:\n                        content = hit.entity.get(\"question\", \"\")\n\n                        memory = TextMemory(\n                            memory_id=hit.entity.get(\"id\"),\n                            content=content,\n                            timestamp=hit.entity.get(\"timestamp\"),\n                        )\n\n                        search_results.append(\n                            TextMemorySearchResult(\n                                memory=memory,\n                                similarity_score=similarity_score,\n                                rank=j + 1,\n                            )\n                        )\n\n            return search_results\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _search)\n\n    async def get_recent_text_memories(\n        self, context: ToolContext, limit: int = 10\n    ) -> List[TextMemory]:\n        \"\"\"Get recently added text memories.\"\"\"\n\n        def _get_recent():\n            collection = self._get_collection()\n\n            # Query text memory entries\n            results = collection.query(\n                expr='tool_name == \"\"',\n                output_fields=[\"id\", \"question\", \"timestamp\"],\n                limit=1000,\n            )\n\n            # Sort by timestamp\n            sorted_results = sorted(\n                results, key=lambda r: r.get(\"timestamp\", \"\"), reverse=True\n            )\n\n            memories = []\n            for result in sorted_results[:limit]:\n                memory = TextMemory(\n                    memory_id=result.get(\"id\"),\n                    content=result.get(\"question\", \"\"),\n                    timestamp=result.get(\"timestamp\"),\n                )\n                memories.append(memory)\n\n            return memories\n\n        return await asyncio.get_event_loop().run_in_executor(\n            self._executor, _get_recent\n        )\n\n    async def delete_text_memory(self, context: ToolContext, memory_id: str) -> bool:\n        \"\"\"Delete a text memory by its ID.\"\"\"\n\n        def _delete():\n            collection = self._get_collection()\n\n            try:\n                expr = f'id == \"{memory_id}\"'\n                collection.delete(expr)\n                return True\n            except Exception:\n                return False\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _delete)\n\n    async def clear_memories(\n        self,\n        context: ToolContext,\n        tool_name: Optional[str] = None,\n        before_date: Optional[str] = None,\n    ) -> int:\n        \"\"\"Clear stored memories.\"\"\"\n\n        def _clear():\n            collection = self._get_collection()\n\n            # Build filter expression\n            expr_parts = []\n            if tool_name:\n                expr_parts.append(f'tool_name == \"{tool_name}\"')\n            if before_date:\n                expr_parts.append(f'timestamp < \"{before_date}\"')\n\n            if expr_parts:\n                expr = \" && \".join(expr_parts)\n            else:\n                expr = \"id != ''\"\n\n            collection.delete(expr)\n            return 0\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _clear)\n"
  },
  {
    "path": "src/vanna/integrations/mock/__init__.py",
    "content": "\"\"\"\nMock integration.\n\nThis module provides mock implementations for testing.\n\"\"\"\n\nfrom .llm import MockLlmService\n\n__all__ = [\"MockLlmService\"]\n"
  },
  {
    "path": "src/vanna/integrations/mock/llm.py",
    "content": "\"\"\"\nMock LLM service implementation for testing.\n\nThis module provides a simple mock implementation of the LlmService interface,\nuseful for testing and development without requiring actual LLM API calls.\n\"\"\"\n\nimport asyncio\nfrom typing import AsyncGenerator, List\n\nfrom vanna.core.llm import LlmService, LlmRequest, LlmResponse, LlmStreamChunk\nfrom vanna.core.tool import ToolSchema\n\n\nclass MockLlmService(LlmService):\n    \"\"\"Mock LLM service that returns predefined responses.\"\"\"\n\n    def __init__(self, response_content: str = \"Hello! This is a mock response.\"):\n        self.response_content = response_content\n        self.call_count = 0\n\n    async def send_request(self, request: LlmRequest) -> LlmResponse:\n        \"\"\"Send a request to the mock LLM.\"\"\"\n        self.call_count += 1\n\n        # Simulate processing delay\n        await asyncio.sleep(0.1)\n\n        # Return a simple response\n        return LlmResponse(\n            content=f\"{self.response_content} (Request #{self.call_count})\",\n            finish_reason=\"stop\",\n            usage={\"prompt_tokens\": 50, \"completion_tokens\": 20, \"total_tokens\": 70},\n        )\n\n    async def stream_request(\n        self, request: LlmRequest\n    ) -> AsyncGenerator[LlmStreamChunk, None]:\n        \"\"\"Stream a request to the mock LLM.\"\"\"\n        self.call_count += 1\n\n        # Split response into chunks\n        words = f\"{self.response_content} (Streamed #{self.call_count})\".split()\n\n        for i, word in enumerate(words):\n            await asyncio.sleep(0.05)  # Simulate streaming delay\n\n            chunk_content = word + (\" \" if i < len(words) - 1 else \"\")\n            yield LlmStreamChunk(\n                content=chunk_content,\n                finish_reason=\"stop\" if i == len(words) - 1 else None,\n            )\n\n    async def validate_tools(self, tools: List[ToolSchema]) -> List[str]:\n        \"\"\"Validate tool schemas and return any errors.\"\"\"\n        # Mock validation - no errors\n        return []\n\n    def set_response(self, content: str) -> None:\n        \"\"\"Set the response content for testing.\"\"\"\n        self.response_content = content\n\n    def reset_call_count(self) -> None:\n        \"\"\"Reset the call counter.\"\"\"\n        self.call_count = 0\n"
  },
  {
    "path": "src/vanna/integrations/mssql/__init__.py",
    "content": "\"\"\"Microsoft SQL Server integration for Vanna.\"\"\"\n\nfrom .sql_runner import MSSQLRunner\n\n__all__ = [\"MSSQLRunner\"]\n"
  },
  {
    "path": "src/vanna/integrations/mssql/sql_runner.py",
    "content": "\"\"\"Microsoft SQL Server implementation of SqlRunner interface.\"\"\"\n\nfrom typing import Optional\nimport pandas as pd\n\nfrom vanna.capabilities.sql_runner import SqlRunner, RunSqlToolArgs\nfrom vanna.core.tool import ToolContext\n\n\nclass MSSQLRunner(SqlRunner):\n    \"\"\"Microsoft SQL Server implementation of the SqlRunner interface.\"\"\"\n\n    def __init__(self, odbc_conn_str: str, **kwargs):\n        \"\"\"Initialize with MSSQL connection parameters.\n\n        Args:\n            odbc_conn_str: The ODBC connection string for SQL Server\n            **kwargs: Additional SQLAlchemy engine parameters\n        \"\"\"\n        try:\n            import pyodbc\n\n            self.pyodbc = pyodbc\n        except ImportError as e:\n            raise ImportError(\n                \"pyodbc package is required. Install with: pip install pyodbc\"\n            ) from e\n\n        try:\n            import sqlalchemy as sa\n            from sqlalchemy.engine import URL\n            from sqlalchemy import create_engine\n\n            self.sa = sa\n            self.URL = URL\n            self.create_engine = create_engine\n        except ImportError as e:\n            raise ImportError(\n                \"sqlalchemy package is required. Install with: pip install sqlalchemy\"\n            ) from e\n\n        # Create the connection URL\n        connection_url = self.URL.create(\n            \"mssql+pyodbc\", query={\"odbc_connect\": odbc_conn_str}\n        )\n\n        # Create the engine\n        self.engine = self.create_engine(connection_url, **kwargs)\n\n    async def run_sql(self, args: RunSqlToolArgs, context: ToolContext) -> pd.DataFrame:\n        \"\"\"Execute SQL query against MSSQL database and return results as DataFrame.\n\n        Args:\n            args: SQL query arguments\n            context: Tool execution context\n\n        Returns:\n            DataFrame with query results\n\n        Raises:\n            sqlalchemy.exc.SQLAlchemyError: If query execution fails\n        \"\"\"\n        # Execute the SQL statement and return the result as a pandas DataFrame\n        with self.engine.begin() as conn:\n            df = pd.read_sql_query(self.sa.text(args.sql), conn)\n            return df\n"
  },
  {
    "path": "src/vanna/integrations/mysql/__init__.py",
    "content": "\"\"\"MySQL integration for Vanna.\"\"\"\n\nfrom .sql_runner import MySQLRunner\n\n__all__ = [\"MySQLRunner\"]\n"
  },
  {
    "path": "src/vanna/integrations/mysql/sql_runner.py",
    "content": "\"\"\"MySQL implementation of SqlRunner interface.\"\"\"\n\nfrom typing import Optional\nimport pandas as pd\n\nfrom vanna.capabilities.sql_runner import SqlRunner, RunSqlToolArgs\nfrom vanna.core.tool import ToolContext\n\n\nclass MySQLRunner(SqlRunner):\n    \"\"\"MySQL implementation of the SqlRunner interface.\"\"\"\n\n    def __init__(\n        self,\n        host: str,\n        database: str,\n        user: str,\n        password: str,\n        port: int = 3306,\n        **kwargs,\n    ):\n        \"\"\"Initialize with MySQL connection parameters.\n\n        Args:\n            host: Database host address\n            database: Database name\n            user: Database user\n            password: Database password\n            port: Database port (default: 3306)\n            **kwargs: Additional PyMySQL connection parameters\n        \"\"\"\n        try:\n            import pymysql.cursors\n\n            self.pymysql = pymysql\n        except ImportError as e:\n            raise ImportError(\n                \"PyMySQL package is required. Install with: pip install 'vanna[mysql]'\"\n            ) from e\n\n        self.host = host\n        self.database = database\n        self.user = user\n        self.password = password\n        self.port = port\n        self.kwargs = kwargs\n\n    async def run_sql(self, args: RunSqlToolArgs, context: ToolContext) -> pd.DataFrame:\n        \"\"\"Execute SQL query against MySQL database and return results as DataFrame.\n\n        Args:\n            args: SQL query arguments\n            context: Tool execution context\n\n        Returns:\n            DataFrame with query results\n\n        Raises:\n            pymysql.Error: If query execution fails\n        \"\"\"\n        # Connect to the database\n        conn = self.pymysql.connect(\n            host=self.host,\n            user=self.user,\n            password=self.password,\n            database=self.database,\n            port=self.port,\n            cursorclass=self.pymysql.cursors.DictCursor,\n            **self.kwargs,\n        )\n\n        try:\n            # Ping to ensure connection is alive\n            conn.ping(reconnect=True)\n\n            cursor = conn.cursor()\n            cursor.execute(args.sql)\n            results = cursor.fetchall()\n\n            # Create a pandas dataframe from the results\n            df = pd.DataFrame(\n                results,\n                columns=[desc[0] for desc in cursor.description]\n                if cursor.description\n                else [],\n            )\n\n            cursor.close()\n            return df\n\n        finally:\n            conn.close()\n"
  },
  {
    "path": "src/vanna/integrations/ollama/__init__.py",
    "content": "\"\"\"\nOllama integration for Vanna Agents.\n\"\"\"\n\nfrom .llm import OllamaLlmService\n\n__all__ = [\"OllamaLlmService\"]\n"
  },
  {
    "path": "src/vanna/integrations/ollama/llm.py",
    "content": "\"\"\"\nOllama LLM service implementation.\n\nThis module provides an implementation of the LlmService interface backed by\nOllama's local LLM API. It supports non-streaming responses and streaming\nof text content. Tool calling support depends on the Ollama model being used.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nfrom typing import Any, AsyncGenerator, Dict, List, Optional\n\nfrom vanna.core.llm import (\n    LlmService,\n    LlmRequest,\n    LlmResponse,\n    LlmStreamChunk,\n)\nfrom vanna.core.tool import ToolCall, ToolSchema\n\n\nclass OllamaLlmService(LlmService):\n    \"\"\"Ollama-backed LLM service for local model inference.\n\n    Args:\n        model: Ollama model name (e.g., \"gpt-oss:20b\").\n        host: Ollama server URL; defaults to \"http://localhost:11434\" or env `OLLAMA_HOST`.\n        timeout: Request timeout in seconds; defaults to 240.\n        num_ctx: Context window size; defaults to 8192.\n        temperature: Sampling temperature; defaults to 0.7.\n        extra_options: Additional options passed to Ollama (e.g., num_predict, top_k, top_p).\n    \"\"\"\n\n    def __init__(\n        self,\n        model: str,\n        host: Optional[str] = None,\n        timeout: float = 240.0,\n        num_ctx: int = 8192,\n        temperature: float = 0.7,\n        **extra_options: Any,\n    ) -> None:\n        try:\n            import ollama\n        except ImportError as e:\n            raise ImportError(\n                \"ollama package is required. Install with: pip install 'vanna[ollama]' or pip install ollama\"\n            ) from e\n\n        if not model:\n            raise ValueError(\"model parameter is required for Ollama\")\n\n        self.model = model\n        self.host = host or os.getenv(\"OLLAMA_HOST\", \"http://localhost:11434\")\n        self.timeout = timeout\n        self.num_ctx = num_ctx\n        self.temperature = temperature\n        self.extra_options = extra_options\n\n        # Create Ollama client\n        self._client = ollama.Client(host=self.host, timeout=timeout)\n\n    async def send_request(self, request: LlmRequest) -> LlmResponse:\n        \"\"\"Send a non-streaming request to Ollama and return the response.\"\"\"\n        payload = self._build_payload(request)\n\n        # Call the Ollama API\n        try:\n            resp = self._client.chat(**payload)\n        except Exception as e:\n            raise RuntimeError(f\"Ollama request failed: {str(e)}\") from e\n\n        # Extract message from response\n        message = resp.get(\"message\", {})\n        content = message.get(\"content\")\n        tool_calls = self._extract_tool_calls_from_message(message)\n\n        # Extract usage information if available\n        usage: Dict[str, int] = {}\n        if \"prompt_eval_count\" in resp or \"eval_count\" in resp:\n            usage = {\n                \"prompt_tokens\": resp.get(\"prompt_eval_count\", 0),\n                \"completion_tokens\": resp.get(\"eval_count\", 0),\n                \"total_tokens\": resp.get(\"prompt_eval_count\", 0)\n                + resp.get(\"eval_count\", 0),\n            }\n\n        return LlmResponse(\n            content=content,\n            tool_calls=tool_calls or None,\n            finish_reason=resp.get(\"done_reason\")\n            or (\"stop\" if resp.get(\"done\") else None),\n            usage=usage or None,\n        )\n\n    async def stream_request(\n        self, request: LlmRequest\n    ) -> AsyncGenerator[LlmStreamChunk, None]:\n        \"\"\"Stream a request to Ollama.\n\n        Emits `LlmStreamChunk` for textual deltas as they arrive. Tool calls are\n        accumulated and emitted in a final chunk when the stream ends.\n        \"\"\"\n        payload = self._build_payload(request)\n\n        # Ollama streaming\n        try:\n            stream = self._client.chat(**payload, stream=True)\n        except Exception as e:\n            raise RuntimeError(f\"Ollama streaming request failed: {str(e)}\") from e\n\n        # Accumulate tool calls if present\n        accumulated_tool_calls: List[ToolCall] = []\n        last_finish: Optional[str] = None\n\n        for chunk in stream:\n            message = chunk.get(\"message\", {})\n\n            # Yield text content\n            content = message.get(\"content\")\n            if content:\n                yield LlmStreamChunk(content=content)\n\n            # Accumulate tool calls\n            tool_calls = self._extract_tool_calls_from_message(message)\n            if tool_calls:\n                accumulated_tool_calls.extend(tool_calls)\n\n            # Track finish reason\n            if chunk.get(\"done\"):\n                last_finish = chunk.get(\"done_reason\", \"stop\")\n\n        # Emit final chunk with tool calls if any\n        if accumulated_tool_calls:\n            yield LlmStreamChunk(\n                tool_calls=accumulated_tool_calls, finish_reason=last_finish or \"stop\"\n            )\n        else:\n            # Emit terminal chunk to signal completion\n            yield LlmStreamChunk(finish_reason=last_finish or \"stop\")\n\n    async def validate_tools(self, tools: List[ToolSchema]) -> List[str]:\n        \"\"\"Validate tool schemas. Returns a list of error messages.\"\"\"\n        errors: List[str] = []\n        # Basic validation; Ollama model support for tools varies\n        for t in tools:\n            if not t.name:\n                errors.append(f\"Tool must have a name\")\n            if not t.description:\n                errors.append(f\"Tool '{t.name}' should have a description\")\n        return errors\n\n    # Internal helpers\n    def _build_payload(self, request: LlmRequest) -> Dict[str, Any]:\n        \"\"\"Build the Ollama chat payload from LlmRequest.\"\"\"\n        messages: List[Dict[str, Any]] = []\n\n        # Add system prompt as first message if provided\n        if request.system_prompt:\n            messages.append({\"role\": \"system\", \"content\": request.system_prompt})\n\n        # Convert messages to Ollama format\n        for m in request.messages:\n            msg: Dict[str, Any] = {\"role\": m.role, \"content\": m.content or \"\"}\n\n            # Handle tool calls in assistant messages\n            if m.role == \"assistant\" and m.tool_calls:\n                # Some Ollama models support tool_calls in message\n                tool_calls_payload = []\n                for tc in m.tool_calls:\n                    tool_calls_payload.append(\n                        {\"function\": {\"name\": tc.name, \"arguments\": tc.arguments}}\n                    )\n                msg[\"tool_calls\"] = tool_calls_payload\n\n            messages.append(msg)\n\n        # Build tools array if tools are provided\n        tools_payload: Optional[List[Dict[str, Any]]] = None\n        if request.tools:\n            tools_payload = []\n            for t in request.tools:\n                tools_payload.append(\n                    {\n                        \"type\": \"function\",\n                        \"function\": {\n                            \"name\": t.name,\n                            \"description\": t.description,\n                            \"parameters\": t.parameters,\n                        },\n                    }\n                )\n\n        # Build options\n        options: Dict[str, Any] = {\n            \"num_ctx\": self.num_ctx,\n            \"temperature\": self.temperature,\n            **self.extra_options,\n        }\n\n        # Build final payload\n        payload: Dict[str, Any] = {\n            \"model\": self.model,\n            \"messages\": messages,\n            \"options\": options,\n        }\n\n        # Add tools if provided (note: not all Ollama models support tools)\n        if tools_payload:\n            payload[\"tools\"] = tools_payload\n\n        return payload\n\n    def _extract_tool_calls_from_message(\n        self, message: Dict[str, Any]\n    ) -> List[ToolCall]:\n        \"\"\"Extract tool calls from Ollama message.\"\"\"\n        tool_calls: List[ToolCall] = []\n\n        # Check for tool_calls in message\n        raw_tool_calls = message.get(\"tool_calls\", [])\n        if not raw_tool_calls:\n            return tool_calls\n\n        for idx, tc in enumerate(raw_tool_calls):\n            fn = tc.get(\"function\", {})\n            name = fn.get(\"name\")\n            if not name:\n                continue\n\n            # Parse arguments\n            arguments = fn.get(\"arguments\", {})\n            if isinstance(arguments, str):\n                try:\n                    arguments = json.loads(arguments)\n                except Exception:\n                    arguments = {\"_raw\": arguments}\n\n            if not isinstance(arguments, dict):\n                arguments = {\"args\": arguments}\n\n            tool_calls.append(\n                ToolCall(\n                    id=tc.get(\"id\", f\"tool_call_{idx}\"),\n                    name=name,\n                    arguments=arguments,\n                )\n            )\n\n        return tool_calls\n"
  },
  {
    "path": "src/vanna/integrations/openai/__init__.py",
    "content": "\"\"\"\nOpenAI integration.\n\nThis module provides OpenAI LLM service implementations.\n\"\"\"\n\nfrom .llm import OpenAILlmService\nfrom .responses import OpenAIResponsesService\n\n__all__ = [\"OpenAILlmService\", \"OpenAIResponsesService\"]\n"
  },
  {
    "path": "src/vanna/integrations/openai/llm.py",
    "content": "\"\"\"\nOpenAI LLM service implementation.\n\nThis module provides an implementation of the LlmService interface backed by\nOpenAI's Chat Completions API (openai>=1.0.0). It supports non-streaming\nresponses and best-effort streaming of text content. Tool/function calling is\npassed through when tools are provided, but full tool-call conversation\nround-tripping may require adding assistant tool-call messages to the\nconversation upstream.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nfrom typing import Any, AsyncGenerator, Dict, List, Optional, cast\n\nfrom vanna.core.llm import (\n    LlmService,\n    LlmRequest,\n    LlmResponse,\n    LlmStreamChunk,\n)\nfrom vanna.core.tool import ToolCall, ToolSchema\n\n\nclass OpenAILlmService(LlmService):\n    \"\"\"OpenAI Chat Completions-backed LLM service.\n\n    Args:\n        model: OpenAI model name (e.g., \"gpt-5\").\n        api_key: API key; falls back to env `OPENAI_API_KEY`.\n        organization: Optional org; env `OPENAI_ORG` if unset.\n        base_url: Optional custom base URL; env `OPENAI_BASE_URL` if unset.\n        extra_client_kwargs: Extra kwargs forwarded to `openai.OpenAI()`.\n    \"\"\"\n\n    def __init__(\n        self,\n        model: Optional[str] = None,\n        api_key: Optional[str] = None,\n        organization: Optional[str] = None,\n        base_url: Optional[str] = None,\n        **extra_client_kwargs: Any,\n    ) -> None:\n        try:\n            from openai import OpenAI\n        except Exception as e:  # pragma: no cover - import-time error surface\n            raise ImportError(\n                \"openai package is required. Install with: pip install 'vanna[openai]'\"\n            ) from e\n\n        self.model = model or os.getenv(\"OPENAI_MODEL\", \"gpt-5\")\n        api_key = api_key or os.getenv(\"OPENAI_API_KEY\")\n        organization = organization or os.getenv(\"OPENAI_ORG\")\n        base_url = base_url or os.getenv(\"OPENAI_BASE_URL\")\n\n        client_kwargs: Dict[str, Any] = {**extra_client_kwargs}\n        if api_key:\n            client_kwargs[\"api_key\"] = api_key\n        if organization:\n            client_kwargs[\"organization\"] = organization\n        if base_url:\n            client_kwargs[\"base_url\"] = base_url\n\n        self._client = OpenAI(**client_kwargs)\n\n    async def send_request(self, request: LlmRequest) -> LlmResponse:\n        \"\"\"Send a non-streaming request to OpenAI and return the response.\"\"\"\n        payload = self._build_payload(request)\n\n        # Call the API synchronously; this function is async but we can block here.\n        resp = self._client.chat.completions.create(**payload, stream=False)\n\n        if not resp.choices:\n            return LlmResponse(content=None, tool_calls=None, finish_reason=None)\n\n        choice = resp.choices[0]\n        content: Optional[str] = getattr(choice.message, \"content\", None)\n        tool_calls = self._extract_tool_calls_from_message(choice.message)\n\n        usage: Dict[str, int] = {}\n        if getattr(resp, \"usage\", None):\n            usage = {\n                k: int(v)\n                for k, v in {\n                    \"prompt_tokens\": getattr(resp.usage, \"prompt_tokens\", 0),\n                    \"completion_tokens\": getattr(resp.usage, \"completion_tokens\", 0),\n                    \"total_tokens\": getattr(resp.usage, \"total_tokens\", 0),\n                }.items()\n            }\n\n        return LlmResponse(\n            content=content,\n            tool_calls=tool_calls or None,\n            finish_reason=getattr(choice, \"finish_reason\", None),\n            usage=usage or None,\n        )\n\n    async def stream_request(\n        self, request: LlmRequest\n    ) -> AsyncGenerator[LlmStreamChunk, None]:\n        \"\"\"Stream a request to OpenAI.\n\n        Emits `LlmStreamChunk` for textual deltas as they arrive. Tool-calls are\n        accumulated and emitted in a final chunk when the stream ends.\n        \"\"\"\n        payload = self._build_payload(request)\n\n        # Synchronous streaming iterator; iterate within async context.\n        stream = self._client.chat.completions.create(**payload, stream=True)\n\n        # Builders for streamed tool-calls (index -> partial)\n        tc_builders: Dict[int, Dict[str, Optional[str]]] = {}\n        last_finish: Optional[str] = None\n\n        for event in stream:\n            if not getattr(event, \"choices\", None):\n                continue\n\n            choice = event.choices[0]\n            delta = getattr(choice, \"delta\", None)\n            if delta is None:\n                # Some SDK versions use `event.choices[0].message` on the final packet\n                last_finish = getattr(choice, \"finish_reason\", last_finish)\n                continue\n\n            # Text content\n            content_piece: Optional[str] = getattr(delta, \"content\", None)\n            if content_piece:\n                yield LlmStreamChunk(content=content_piece)\n\n            # Tool calls (streamed)\n            streamed_tool_calls = getattr(delta, \"tool_calls\", None)\n            if streamed_tool_calls:\n                for tc in streamed_tool_calls:\n                    idx = getattr(tc, \"index\", 0) or 0\n                    b = tc_builders.setdefault(\n                        idx, {\"id\": None, \"name\": None, \"arguments\": \"\"}\n                    )\n                    if getattr(tc, \"id\", None):\n                        b[\"id\"] = tc.id\n                    fn = getattr(tc, \"function\", None)\n                    if fn is not None:\n                        if getattr(fn, \"name\", None):\n                            b[\"name\"] = fn.name\n                        if getattr(fn, \"arguments\", None):\n                            b[\"arguments\"] = (b[\"arguments\"] or \"\") + fn.arguments\n\n            last_finish = getattr(choice, \"finish_reason\", last_finish)\n\n        # Emit final tool-calls chunk if any\n        final_tool_calls: List[ToolCall] = []\n        for b in tc_builders.values():\n            if not b.get(\"name\"):\n                continue\n            args_raw = b.get(\"arguments\") or \"{}\"\n            try:\n                loaded = json.loads(args_raw)\n                if isinstance(loaded, dict):\n                    args_dict: Dict[str, Any] = loaded\n                else:\n                    args_dict = {\"args\": loaded}\n            except Exception:\n                args_dict = {\"_raw\": args_raw}\n            final_tool_calls.append(\n                ToolCall(\n                    id=b.get(\"id\") or \"tool_call\",\n                    name=b[\"name\"] or \"tool\",\n                    arguments=args_dict,\n                )\n            )\n\n        if final_tool_calls:\n            yield LlmStreamChunk(tool_calls=final_tool_calls, finish_reason=last_finish)\n        else:\n            # Still emit a terminal chunk to signal completion\n            yield LlmStreamChunk(finish_reason=last_finish or \"stop\")\n\n    async def validate_tools(self, tools: List[ToolSchema]) -> List[str]:\n        \"\"\"Validate tool schemas. Returns a list of error messages.\"\"\"\n        errors: List[str] = []\n        # Basic checks; OpenAI will enforce further validation server-side.\n        for t in tools:\n            if not t.name or len(t.name) > 64:\n                errors.append(f\"Invalid tool name: {t.name!r}\")\n        return errors\n\n    # Internal helpers\n    def _build_payload(self, request: LlmRequest) -> Dict[str, Any]:\n        messages: List[Dict[str, Any]] = []\n\n        # Add system prompt as first message if provided\n        if request.system_prompt:\n            messages.append({\"role\": \"system\", \"content\": request.system_prompt})\n\n        for m in request.messages:\n            msg: Dict[str, Any] = {\"role\": m.role, \"content\": m.content}\n            if m.role == \"tool\" and m.tool_call_id:\n                msg[\"tool_call_id\"] = m.tool_call_id\n            elif m.role == \"assistant\" and m.tool_calls:\n                # Convert tool calls to OpenAI format\n                tool_calls_payload = []\n                for tc in m.tool_calls:\n                    tool_calls_payload.append(\n                        {\n                            \"id\": tc.id,\n                            \"type\": \"function\",\n                            \"function\": {\n                                \"name\": tc.name,\n                                \"arguments\": json.dumps(tc.arguments),\n                            },\n                        }\n                    )\n                msg[\"tool_calls\"] = tool_calls_payload\n            messages.append(msg)\n\n        tools_payload: Optional[List[Dict[str, Any]]] = None\n        if request.tools:\n            tools_payload = [\n                {\n                    \"type\": \"function\",\n                    \"function\": {\n                        \"name\": t.name,\n                        \"description\": t.description,\n                        \"parameters\": t.parameters,\n                    },\n                }\n                for t in request.tools\n            ]\n\n        payload: Dict[str, Any] = {\n            \"model\": self.model,\n            \"messages\": messages,\n        }\n        if request.max_tokens is not None:\n            payload[\"max_tokens\"] = request.max_tokens\n        if tools_payload:\n            payload[\"tools\"] = tools_payload\n            payload[\"tool_choice\"] = \"auto\"\n\n        return payload\n\n    def _extract_tool_calls_from_message(self, message: Any) -> List[ToolCall]:\n        tool_calls: List[ToolCall] = []\n        raw_tool_calls = getattr(message, \"tool_calls\", None) or []\n        for tc in raw_tool_calls:\n            fn = getattr(tc, \"function\", None)\n            if not fn:\n                continue\n            args_raw = getattr(fn, \"arguments\", \"{}\")\n            try:\n                loaded = json.loads(args_raw)\n                if isinstance(loaded, dict):\n                    args_dict: Dict[str, Any] = loaded\n                else:\n                    args_dict = {\"args\": loaded}\n            except Exception:\n                args_dict = {\"_raw\": args_raw}\n            tool_calls.append(\n                ToolCall(\n                    id=getattr(tc, \"id\", \"tool_call\"),\n                    name=getattr(fn, \"name\", \"tool\"),\n                    arguments=args_dict,\n                )\n            )\n        return tool_calls\n"
  },
  {
    "path": "src/vanna/integrations/openai/responses.py",
    "content": "from __future__ import annotations\n\nimport json\nimport os\nfrom typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, TYPE_CHECKING\n\nfrom vanna.core.llm import LlmService, LlmRequest, LlmResponse, LlmStreamChunk\nfrom vanna.core.tool import ToolCall, ToolSchema\n\nif TYPE_CHECKING:\n    from openai.types.responses import Response\n\n\nclass OpenAIResponsesService(LlmService):\n    def __init__(\n        self, api_key: Optional[str] = None, model: Optional[str] = None\n    ) -> None:\n        try:\n            from openai import AsyncOpenAI\n            from openai.types.responses import Response\n        except Exception as e:  # pragma: no cover\n            raise ImportError(\n                \"openai package is required. Install with: pip install 'vanna[openai]'\"\n            ) from e\n\n        self.client = AsyncOpenAI(api_key=api_key or os.getenv(\"OPENAI_API_KEY\"))\n        self.model = model or os.getenv(\"OPENAI_MODEL\", \"gpt-5\")\n\n    async def send_request(self, request: LlmRequest) -> LlmResponse:\n        payload = self._payload(request)\n        resp: Response = await self.client.responses.create(**payload)\n        self._debug_print(\"response\", resp)\n        text, tools, status, usage = self._extract(resp)\n        return LlmResponse(\n            content=text,\n            tool_calls=tools or None,\n            finish_reason=status,\n            usage=usage or None,\n            metadata={\"request_id\": getattr(resp, \"id\", None)},\n        )\n\n    async def stream_request(\n        self, request: LlmRequest\n    ) -> AsyncGenerator[LlmStreamChunk, None]:\n        payload = self._payload(request)\n        async with self.client.responses.stream(**payload) as stream:\n            async for event in stream:\n                self._debug_print(\"stream_event\", event)\n                event_type = getattr(event, \"type\", None)\n                if event_type == \"response.output_text.delta\":\n                    delta = getattr(event, \"delta\", None)\n                    if delta:\n                        yield LlmStreamChunk(content=delta)\n            final: Response = await stream.get_final_response()\n            self._debug_print(\"final_response\", final)\n\n        _text, tools, status, _usage = self._extract(final)\n        yield LlmStreamChunk(tool_calls=tools or None, finish_reason=status)\n\n    async def validate_tools(self, tools: List[Any]) -> List[str]:\n        return []  # minimal: accept whatever's passed through\n\n    # ---- helpers ----\n\n    def _payload(self, request: LlmRequest) -> Dict[str, Any]:\n        msgs = [{\"role\": m.role, \"content\": m.content} for m in request.messages]\n        p: Dict[str, Any] = {\"model\": self.model, \"input\": msgs}\n        if request.system_prompt:\n            p[\"instructions\"] = request.system_prompt\n        if request.max_tokens:\n            p[\"max_output_tokens\"] = request.max_tokens\n        if request.tools:\n            p[\"tools\"] = [self._serialize_tool(t) for t in request.tools]\n        return p\n\n    def _debug_print(self, label: str, obj: Any) -> None:\n        try:\n            payload = obj.model_dump()\n        except AttributeError:\n            try:\n                payload = obj.dict()\n            except AttributeError:\n                payload = obj\n        print(f\"[OpenAIResponsesService] {label}: {payload}\")\n\n    def _extract(\n        self, resp: Response\n    ) -> Tuple[\n        Optional[str], Optional[List[ToolCall]], Optional[str], Optional[Dict[str, int]]\n    ]:\n        text = getattr(resp, \"output_text\", None)\n\n        tool_calls: List[ToolCall] = []\n        for oc in getattr(resp, \"output\", []) or []:\n            for item in getattr(oc, \"content\", []) or []:\n                if getattr(item, \"type\", None) == \"tool_call\":\n                    tc = getattr(item, \"tool_call\", None)\n                    if tc and getattr(tc, \"type\", None) == \"function\":\n                        fn = getattr(tc, \"function\", None)\n                        if fn:\n                            name = getattr(fn, \"name\", None)\n                            args = getattr(fn, \"arguments\", None)\n                            if not isinstance(args, (dict, list)):\n                                try:\n                                    args = json.loads(args) if args else {}\n                                except Exception:\n                                    args = {\"_raw\": args}\n                            tool_calls.append(ToolCall(name=name, arguments=args))\n\n        usage = None\n        if getattr(resp, \"usage\", None):\n            usage = {\n                \"input_tokens\": getattr(resp.usage, \"input_tokens\", 0) or 0,\n                \"output_tokens\": getattr(resp.usage, \"output_tokens\", 0) or 0,\n                \"total_tokens\": getattr(resp.usage, \"total_tokens\", None)\n                or (\n                    (getattr(resp.usage, \"input_tokens\", 0) or 0)\n                    + (getattr(resp.usage, \"output_tokens\", 0) or 0)\n                ),\n            }\n\n        status = getattr(resp, \"status\", None)  # e.g. \"completed\"\n        return text, (tool_calls or None), status, usage\n\n    def _serialize_tool(self, tool: Any) -> Dict[str, Any]:\n        \"\"\"Convert a tool schema into the dict format expected by OpenAI Responses.\"\"\"\n\n        if isinstance(tool, ToolSchema):\n            return {\n                \"type\": \"function\",\n                \"name\": tool.name,\n                \"description\": tool.description,\n                \"parameters\": tool.parameters,\n                \"strict\": False,\n            }\n\n        # Support generic pydantic/BaseModel style objects without importing pydantic here.\n        if hasattr(tool, \"model_dump\"):\n            data = tool.model_dump()\n            if all(key in data for key in (\"name\", \"description\", \"parameters\")):\n                return {\n                    \"type\": \"function\",\n                    \"name\": data[\"name\"],\n                    \"description\": data[\"description\"],\n                    \"parameters\": data[\"parameters\"],\n                    \"strict\": data.get(\"strict\", False),\n                }\n            return data\n\n        if isinstance(tool, dict):\n            if \"type\" in tool:\n                return tool\n            if all(k in tool for k in (\"name\", \"description\", \"parameters\")):\n                return {\n                    \"type\": \"function\",\n                    \"name\": tool[\"name\"],\n                    \"description\": tool[\"description\"],\n                    \"parameters\": tool[\"parameters\"],\n                    \"strict\": tool.get(\"strict\", False),\n                }\n            return tool\n\n        raise TypeError(f\"Unsupported tool schema type: {type(tool)!r}\")\n"
  },
  {
    "path": "src/vanna/integrations/opensearch/__init__.py",
    "content": "\"\"\"\nOpenSearch integration for Vanna Agents.\n\"\"\"\n\nfrom .agent_memory import OpenSearchAgentMemory\n\n__all__ = [\"OpenSearchAgentMemory\"]\n"
  },
  {
    "path": "src/vanna/integrations/opensearch/agent_memory.py",
    "content": "\"\"\"\nOpenSearch vector database implementation of AgentMemory.\n\nThis implementation uses OpenSearch for distributed search and storage of tool usage patterns.\n\"\"\"\n\nimport json\nimport uuid\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional\nimport asyncio\nfrom concurrent.futures import ThreadPoolExecutor\n\ntry:\n    from opensearchpy import OpenSearch, helpers\n\n    OPENSEARCH_AVAILABLE = True\nexcept ImportError:\n    OPENSEARCH_AVAILABLE = False\n\nfrom vanna.capabilities.agent_memory import (\n    AgentMemory,\n    TextMemory,\n    TextMemorySearchResult,\n    ToolMemory,\n    ToolMemorySearchResult,\n)\nfrom vanna.core.tool import ToolContext\n\n\nclass OpenSearchAgentMemory(AgentMemory):\n    \"\"\"OpenSearch-based implementation of AgentMemory.\"\"\"\n\n    def __init__(\n        self,\n        index_name: str = \"tool_memories\",\n        hosts: Optional[List[str]] = None,\n        http_auth: Optional[tuple] = None,\n        use_ssl: bool = False,\n        verify_certs: bool = False,\n        dimension: int = 384,\n    ):\n        if not OPENSEARCH_AVAILABLE:\n            raise ImportError(\n                \"OpenSearch is required for OpenSearchAgentMemory. Install with: pip install opensearch-py\"\n            )\n\n        self.index_name = index_name\n        self.hosts = hosts or [\"localhost:9200\"]\n        self.http_auth = http_auth\n        self.use_ssl = use_ssl\n        self.verify_certs = verify_certs\n        self.dimension = dimension\n        self._client = None\n        self._executor = ThreadPoolExecutor(max_workers=2)\n\n    def _get_client(self):\n        \"\"\"Get or create OpenSearch client.\"\"\"\n        if self._client is None:\n            self._client = OpenSearch(\n                hosts=self.hosts,\n                http_auth=self.http_auth,\n                use_ssl=self.use_ssl,\n                verify_certs=self.verify_certs,\n                ssl_show_warn=False,\n            )\n\n            # Create index if it doesn't exist\n            if not self._client.indices.exists(index=self.index_name):\n                index_body = {\n                    \"settings\": {\n                        \"index\": {\"knn\": True, \"knn.algo_param.ef_search\": 100}\n                    },\n                    \"mappings\": {\n                        \"properties\": {\n                            \"memory_id\": {\"type\": \"keyword\"},\n                            \"question\": {\"type\": \"text\"},\n                            \"tool_name\": {\"type\": \"keyword\"},\n                            \"args\": {\"type\": \"object\", \"enabled\": False},\n                            \"timestamp\": {\"type\": \"date\"},\n                            \"success\": {\"type\": \"boolean\"},\n                            \"metadata\": {\"type\": \"object\", \"enabled\": False},\n                            \"embedding\": {\n                                \"type\": \"knn_vector\",\n                                \"dimension\": self.dimension,\n                                \"method\": {\n                                    \"name\": \"hnsw\",\n                                    \"space_type\": \"cosinesimil\",\n                                    \"engine\": \"nmslib\",\n                                },\n                            },\n                        }\n                    },\n                }\n                self._client.indices.create(index=self.index_name, body=index_body)\n\n        return self._client\n\n    def _create_embedding(self, text: str) -> List[float]:\n        \"\"\"Create a simple embedding from text (placeholder).\"\"\"\n        import hashlib\n\n        hash_val = int(hashlib.md5(text.encode()).hexdigest(), 16)\n        return [(hash_val >> i) % 100 / 100.0 for i in range(self.dimension)]\n\n    async def save_tool_usage(\n        self,\n        question: str,\n        tool_name: str,\n        args: Dict[str, Any],\n        context: ToolContext,\n        success: bool = True,\n        metadata: Optional[Dict[str, Any]] = None,\n    ) -> None:\n        \"\"\"Save a tool usage pattern.\"\"\"\n\n        def _save():\n            client = self._get_client()\n\n            memory_id = str(uuid.uuid4())\n            timestamp = datetime.now().isoformat()\n            embedding = self._create_embedding(question)\n\n            document = {\n                \"memory_id\": memory_id,\n                \"question\": question,\n                \"tool_name\": tool_name,\n                \"args\": args,\n                \"timestamp\": timestamp,\n                \"success\": success,\n                \"metadata\": metadata or {},\n                \"embedding\": embedding,\n            }\n\n            client.index(\n                index=self.index_name, body=document, id=memory_id, refresh=True\n            )\n\n        await asyncio.get_event_loop().run_in_executor(self._executor, _save)\n\n    async def search_similar_usage(\n        self,\n        question: str,\n        context: ToolContext,\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n        tool_name_filter: Optional[str] = None,\n    ) -> List[ToolMemorySearchResult]:\n        \"\"\"Search for similar tool usage patterns.\"\"\"\n\n        def _search():\n            client = self._get_client()\n\n            embedding = self._create_embedding(question)\n\n            # Build query\n            must_conditions = [{\"term\": {\"success\": True}}]\n            if tool_name_filter:\n                must_conditions.append({\"term\": {\"tool_name\": tool_name_filter}})\n\n            query = {\n                \"size\": limit,\n                \"query\": {\n                    \"bool\": {\n                        \"must\": must_conditions,\n                        \"filter\": {\n                            \"knn\": {\"embedding\": {\"vector\": embedding, \"k\": limit}}\n                        },\n                    }\n                },\n            }\n\n            response = client.search(index=self.index_name, body=query)\n\n            search_results = []\n            for i, hit in enumerate(response[\"hits\"][\"hits\"]):\n                source = hit[\"_source\"]\n                score = hit[\"_score\"]\n\n                # Normalize score to 0-1 range (OpenSearch scores can vary)\n                similarity_score = min(score / 10.0, 1.0)\n\n                if similarity_score >= similarity_threshold:\n                    memory = ToolMemory(\n                        memory_id=source[\"memory_id\"],\n                        question=source[\"question\"],\n                        tool_name=source[\"tool_name\"],\n                        args=source[\"args\"],\n                        timestamp=source.get(\"timestamp\"),\n                        success=source.get(\"success\", True),\n                        metadata=source.get(\"metadata\", {}),\n                    )\n\n                    search_results.append(\n                        ToolMemorySearchResult(\n                            memory=memory, similarity_score=similarity_score, rank=i + 1\n                        )\n                    )\n\n            return search_results\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _search)\n\n    async def get_recent_memories(\n        self, context: ToolContext, limit: int = 10\n    ) -> List[ToolMemory]:\n        \"\"\"Get recently added memories.\"\"\"\n\n        def _get_recent():\n            client = self._get_client()\n\n            query = {\n                \"size\": limit,\n                \"query\": {\"match_all\": {}},\n                \"sort\": [{\"timestamp\": {\"order\": \"desc\"}}],\n            }\n\n            response = client.search(index=self.index_name, body=query)\n\n            memories = []\n            for hit in response[\"hits\"][\"hits\"]:\n                source = hit[\"_source\"]\n\n                memory = ToolMemory(\n                    memory_id=source[\"memory_id\"],\n                    question=source[\"question\"],\n                    tool_name=source[\"tool_name\"],\n                    args=source[\"args\"],\n                    timestamp=source.get(\"timestamp\"),\n                    success=source.get(\"success\", True),\n                    metadata=source.get(\"metadata\", {}),\n                )\n                memories.append(memory)\n\n            return memories\n\n        return await asyncio.get_event_loop().run_in_executor(\n            self._executor, _get_recent\n        )\n\n    async def delete_by_id(self, context: ToolContext, memory_id: str) -> bool:\n        \"\"\"Delete a memory by its ID.\"\"\"\n\n        def _delete():\n            client = self._get_client()\n\n            try:\n                client.delete(index=self.index_name, id=memory_id, refresh=True)\n                return True\n            except Exception:\n                return False\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _delete)\n\n    async def save_text_memory(self, content: str, context: ToolContext) -> TextMemory:\n        \"\"\"Save a text memory.\"\"\"\n\n        def _save():\n            client = self._get_client()\n\n            memory_id = str(uuid.uuid4())\n            timestamp = datetime.now().isoformat()\n            embedding = self._create_embedding(content)\n\n            document = {\n                \"memory_id\": memory_id,\n                \"content\": content,\n                \"timestamp\": timestamp,\n                \"is_text_memory\": True,\n                \"embedding\": embedding,\n            }\n\n            client.index(\n                index=self.index_name, body=document, id=memory_id, refresh=True\n            )\n\n            return TextMemory(memory_id=memory_id, content=content, timestamp=timestamp)\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _save)\n\n    async def search_text_memories(\n        self,\n        query: str,\n        context: ToolContext,\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n    ) -> List[TextMemorySearchResult]:\n        \"\"\"Search for similar text memories.\"\"\"\n\n        def _search():\n            client = self._get_client()\n\n            embedding = self._create_embedding(query)\n\n            query_body = {\n                \"size\": limit,\n                \"query\": {\n                    \"bool\": {\n                        \"must\": [{\"term\": {\"is_text_memory\": True}}],\n                        \"filter\": {\n                            \"knn\": {\"embedding\": {\"vector\": embedding, \"k\": limit}}\n                        },\n                    }\n                },\n            }\n\n            response = client.search(index=self.index_name, body=query_body)\n\n            search_results = []\n            for i, hit in enumerate(response[\"hits\"][\"hits\"]):\n                source = hit[\"_source\"]\n                score = hit[\"_score\"]\n\n                similarity_score = min(score / 10.0, 1.0)\n\n                if similarity_score >= similarity_threshold:\n                    memory = TextMemory(\n                        memory_id=source[\"memory_id\"],\n                        content=source.get(\"content\", \"\"),\n                        timestamp=source.get(\"timestamp\"),\n                    )\n\n                    search_results.append(\n                        TextMemorySearchResult(\n                            memory=memory, similarity_score=similarity_score, rank=i + 1\n                        )\n                    )\n\n            return search_results\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _search)\n\n    async def get_recent_text_memories(\n        self, context: ToolContext, limit: int = 10\n    ) -> List[TextMemory]:\n        \"\"\"Get recently added text memories.\"\"\"\n\n        def _get_recent():\n            client = self._get_client()\n\n            query = {\n                \"size\": limit,\n                \"query\": {\"term\": {\"is_text_memory\": True}},\n                \"sort\": [{\"timestamp\": {\"order\": \"desc\"}}],\n            }\n\n            response = client.search(index=self.index_name, body=query)\n\n            memories = []\n            for hit in response[\"hits\"][\"hits\"]:\n                source = hit[\"_source\"]\n\n                memory = TextMemory(\n                    memory_id=source[\"memory_id\"],\n                    content=source.get(\"content\", \"\"),\n                    timestamp=source.get(\"timestamp\"),\n                )\n                memories.append(memory)\n\n            return memories\n\n        return await asyncio.get_event_loop().run_in_executor(\n            self._executor, _get_recent\n        )\n\n    async def delete_text_memory(self, context: ToolContext, memory_id: str) -> bool:\n        \"\"\"Delete a text memory by its ID.\"\"\"\n\n        def _delete():\n            client = self._get_client()\n\n            try:\n                client.delete(index=self.index_name, id=memory_id, refresh=True)\n                return True\n            except Exception:\n                return False\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _delete)\n\n    async def clear_memories(\n        self,\n        context: ToolContext,\n        tool_name: Optional[str] = None,\n        before_date: Optional[str] = None,\n    ) -> int:\n        \"\"\"Clear stored memories.\"\"\"\n\n        def _clear():\n            client = self._get_client()\n\n            # Build query\n            must_conditions = []\n            if tool_name:\n                must_conditions.append({\"term\": {\"tool_name\": tool_name}})\n            if before_date:\n                must_conditions.append({\"range\": {\"timestamp\": {\"lt\": before_date}}})\n\n            if must_conditions:\n                query = {\"query\": {\"bool\": {\"must\": must_conditions}}}\n            else:\n                query = {\"query\": {\"match_all\": {}}}\n\n            response = client.delete_by_query(\n                index=self.index_name, body=query, refresh=True\n            )\n\n            return response.get(\"deleted\", 0)\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _clear)\n"
  },
  {
    "path": "src/vanna/integrations/oracle/__init__.py",
    "content": "\"\"\"Oracle integration for Vanna.\"\"\"\n\nfrom .sql_runner import OracleRunner\n\n__all__ = [\"OracleRunner\"]\n"
  },
  {
    "path": "src/vanna/integrations/oracle/sql_runner.py",
    "content": "\"\"\"Oracle implementation of SqlRunner interface.\"\"\"\n\nfrom typing import Optional\nimport pandas as pd\n\nfrom vanna.capabilities.sql_runner import SqlRunner, RunSqlToolArgs\nfrom vanna.core.tool import ToolContext\n\n\nclass OracleRunner(SqlRunner):\n    \"\"\"Oracle implementation of the SqlRunner interface.\"\"\"\n\n    def __init__(self, user: str, password: str, dsn: str, **kwargs):\n        \"\"\"Initialize with Oracle connection parameters.\n\n        Args:\n            user: Oracle database user name\n            password: Oracle database user password\n            dsn: Oracle database host - format: host:port/sid\n            **kwargs: Additional oracledb connection parameters\n        \"\"\"\n        try:\n            import oracledb\n\n            self.oracledb = oracledb\n        except ImportError as e:\n            raise ImportError(\n                \"oracledb package is required. Install with: pip install 'vanna[oracle]'\"\n            ) from e\n\n        self.user = user\n        self.password = password\n        self.dsn = dsn\n        self.kwargs = kwargs\n\n    async def run_sql(self, args: RunSqlToolArgs, context: ToolContext) -> pd.DataFrame:\n        \"\"\"Execute SQL query against Oracle database and return results as DataFrame.\n\n        Args:\n            args: SQL query arguments\n            context: Tool execution context\n\n        Returns:\n            DataFrame with query results\n\n        Raises:\n            oracledb.Error: If query execution fails\n        \"\"\"\n        # Connect to the database\n        conn = self.oracledb.connect(\n            user=self.user, password=self.password, dsn=self.dsn, **self.kwargs\n        )\n\n        cursor = conn.cursor()\n\n        try:\n            # Strip and remove trailing semicolons (Oracle doesn't like them)\n            sql = args.sql.rstrip()\n            if sql.endswith(\";\"):\n                sql = sql[:-1]\n\n            # Execute the query\n            cursor.execute(sql)\n            results = cursor.fetchall()\n\n            # Create a pandas dataframe from the results\n            df = pd.DataFrame(results, columns=[desc[0] for desc in cursor.description])\n            return df\n\n        except self.oracledb.Error:\n            conn.rollback()\n            raise\n        finally:\n            cursor.close()\n            conn.close()\n"
  },
  {
    "path": "src/vanna/integrations/pinecone/__init__.py",
    "content": "\"\"\"\nPinecone integration for Vanna Agents.\n\"\"\"\n\nfrom .agent_memory import PineconeAgentMemory\n\n__all__ = [\"PineconeAgentMemory\"]\n"
  },
  {
    "path": "src/vanna/integrations/pinecone/agent_memory.py",
    "content": "\"\"\"\nPinecone vector database implementation of AgentMemory.\n\nThis implementation uses Pinecone for cloud-based vector storage of tool usage patterns.\n\"\"\"\n\nimport json\nimport uuid\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional\nimport asyncio\nfrom concurrent.futures import ThreadPoolExecutor\n\ntry:\n    from pinecone import Pinecone, ServerlessSpec\n\n    PINECONE_AVAILABLE = True\nexcept ImportError:\n    PINECONE_AVAILABLE = False\n\nfrom vanna.capabilities.agent_memory import (\n    AgentMemory,\n    TextMemory,\n    TextMemorySearchResult,\n    ToolMemory,\n    ToolMemorySearchResult,\n)\nfrom vanna.core.tool import ToolContext\n\n\nclass PineconeAgentMemory(AgentMemory):\n    \"\"\"Pinecone-based implementation of AgentMemory.\"\"\"\n\n    def __init__(\n        self,\n        api_key: str,\n        index_name: str = \"tool-memories\",\n        environment: str = \"us-east-1\",\n        dimension: int = 384,\n        metric: str = \"cosine\",\n    ):\n        if not PINECONE_AVAILABLE:\n            raise ImportError(\n                \"Pinecone is required for PineconeAgentMemory. Install with: pip install pinecone-client\"\n            )\n\n        self.api_key = api_key\n        self.index_name = index_name\n        self.environment = environment\n        self.dimension = dimension\n        self.metric = metric\n        self._client = None\n        self._index = None\n        self._executor = ThreadPoolExecutor(max_workers=2)\n\n    def _get_client(self):\n        \"\"\"Get or create Pinecone client.\"\"\"\n        if self._client is None:\n            self._client = Pinecone(api_key=self.api_key)\n        return self._client\n\n    def _get_index(self):\n        \"\"\"Get or create Pinecone index.\"\"\"\n        if self._index is None:\n            client = self._get_client()\n\n            # Create index if it doesn't exist\n            if self.index_name not in client.list_indexes().names():\n                client.create_index(\n                    name=self.index_name,\n                    dimension=self.dimension,\n                    metric=self.metric,\n                    spec=ServerlessSpec(cloud=\"aws\", region=self.environment),\n                )\n\n            self._index = client.Index(self.index_name)\n        return self._index\n\n    def _create_embedding(self, text: str) -> List[float]:\n        \"\"\"Create a simple embedding from text (placeholder - should use actual embedding model).\"\"\"\n        # TODO: Replace with actual embedding model\n        import hashlib\n\n        hash_val = int(hashlib.md5(text.encode()).hexdigest(), 16)\n        return [(hash_val >> i) % 100 / 100.0 for i in range(self.dimension)]\n\n    async def save_tool_usage(\n        self,\n        question: str,\n        tool_name: str,\n        args: Dict[str, Any],\n        context: ToolContext,\n        success: bool = True,\n        metadata: Optional[Dict[str, Any]] = None,\n    ) -> None:\n        \"\"\"Save a tool usage pattern.\"\"\"\n\n        def _save():\n            index = self._get_index()\n\n            memory_id = str(uuid.uuid4())\n            timestamp = datetime.now().isoformat()\n            embedding = self._create_embedding(question)\n\n            # Pinecone metadata must be simple types\n            memory_metadata = {\n                \"question\": question,\n                \"tool_name\": tool_name,\n                \"args_json\": json.dumps(args),\n                \"timestamp\": timestamp,\n                \"success\": success,\n                \"metadata_json\": json.dumps(metadata or {}),\n            }\n\n            index.upsert(vectors=[(memory_id, embedding, memory_metadata)])\n\n        await asyncio.get_event_loop().run_in_executor(self._executor, _save)\n\n    async def search_similar_usage(\n        self,\n        question: str,\n        context: ToolContext,\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n        tool_name_filter: Optional[str] = None,\n    ) -> List[ToolMemorySearchResult]:\n        \"\"\"Search for similar tool usage patterns.\"\"\"\n\n        def _search():\n            index = self._get_index()\n\n            embedding = self._create_embedding(question)\n\n            # Build filter\n            filter_dict = {\"success\": True}\n            if tool_name_filter:\n                filter_dict[\"tool_name\"] = tool_name_filter\n\n            results = index.query(\n                vector=embedding, top_k=limit, filter=filter_dict, include_metadata=True\n            )\n\n            search_results = []\n            for i, match in enumerate(results.matches):\n                # Pinecone returns similarity score directly\n                similarity_score = match.score\n\n                if similarity_score >= similarity_threshold:\n                    metadata = match.metadata\n                    args = json.loads(metadata.get(\"args_json\", \"{}\"))\n                    metadata_dict = json.loads(metadata.get(\"metadata_json\", \"{}\"))\n\n                    memory = ToolMemory(\n                        memory_id=match.id,\n                        question=metadata[\"question\"],\n                        tool_name=metadata[\"tool_name\"],\n                        args=args,\n                        timestamp=metadata.get(\"timestamp\"),\n                        success=metadata.get(\"success\", True),\n                        metadata=metadata_dict,\n                    )\n\n                    search_results.append(\n                        ToolMemorySearchResult(\n                            memory=memory, similarity_score=similarity_score, rank=i + 1\n                        )\n                    )\n\n            return search_results\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _search)\n\n    async def get_recent_memories(\n        self, context: ToolContext, limit: int = 10\n    ) -> List[ToolMemory]:\n        \"\"\"Get recently added memories.\"\"\"\n\n        def _get_recent():\n            index = self._get_index()\n\n            # Pinecone doesn't have a native \"get all\" - we need to query with a dummy vector\n            # or use the list operation with metadata filtering\n            # This is a limitation - we'll return empty for now\n            # In production, you'd maintain a separate timestamp index or use Pinecone's metadata filtering\n            return []\n\n        return await asyncio.get_event_loop().run_in_executor(\n            self._executor, _get_recent\n        )\n\n    async def delete_by_id(self, context: ToolContext, memory_id: str) -> bool:\n        \"\"\"Delete a memory by its ID.\"\"\"\n\n        def _delete():\n            index = self._get_index()\n\n            try:\n                index.delete(ids=[memory_id])\n                return True\n            except Exception:\n                return False\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _delete)\n\n    async def save_text_memory(self, content: str, context: ToolContext) -> TextMemory:\n        \"\"\"Save a text memory.\"\"\"\n\n        def _save():\n            index = self._get_index()\n\n            memory_id = str(uuid.uuid4())\n            timestamp = datetime.now().isoformat()\n            embedding = self._create_embedding(content)\n\n            memory_metadata = {\n                \"content\": content,\n                \"timestamp\": timestamp,\n                \"is_text_memory\": True,\n            }\n\n            index.upsert(vectors=[(memory_id, embedding, memory_metadata)])\n\n            return TextMemory(memory_id=memory_id, content=content, timestamp=timestamp)\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _save)\n\n    async def search_text_memories(\n        self,\n        query: str,\n        context: ToolContext,\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n    ) -> List[TextMemorySearchResult]:\n        \"\"\"Search for similar text memories.\"\"\"\n\n        def _search():\n            index = self._get_index()\n\n            embedding = self._create_embedding(query)\n\n            filter_dict = {\"is_text_memory\": True}\n\n            results = index.query(\n                vector=embedding, top_k=limit, filter=filter_dict, include_metadata=True\n            )\n\n            search_results = []\n            for i, match in enumerate(results.matches):\n                similarity_score = match.score\n\n                if similarity_score >= similarity_threshold:\n                    metadata = match.metadata\n\n                    memory = TextMemory(\n                        memory_id=match.id,\n                        content=metadata.get(\"content\", \"\"),\n                        timestamp=metadata.get(\"timestamp\"),\n                    )\n\n                    search_results.append(\n                        TextMemorySearchResult(\n                            memory=memory, similarity_score=similarity_score, rank=i + 1\n                        )\n                    )\n\n            return search_results\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _search)\n\n    async def get_recent_text_memories(\n        self, context: ToolContext, limit: int = 10\n    ) -> List[TextMemory]:\n        \"\"\"Get recently added text memories.\"\"\"\n\n        def _get_recent():\n            # Pinecone doesn't have a native \"get all sorted by timestamp\" operation\n            # This is a limitation - returning empty list\n            # In production, you'd need to maintain a separate index or use metadata filtering\n            return []\n\n        return await asyncio.get_event_loop().run_in_executor(\n            self._executor, _get_recent\n        )\n\n    async def delete_text_memory(self, context: ToolContext, memory_id: str) -> bool:\n        \"\"\"Delete a text memory by its ID.\"\"\"\n\n        def _delete():\n            index = self._get_index()\n\n            try:\n                index.delete(ids=[memory_id])\n                return True\n            except Exception:\n                return False\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _delete)\n\n    async def clear_memories(\n        self,\n        context: ToolContext,\n        tool_name: Optional[str] = None,\n        before_date: Optional[str] = None,\n    ) -> int:\n        \"\"\"Clear stored memories.\"\"\"\n\n        def _clear():\n            index = self._get_index()\n\n            # Build filter\n            filter_dict = {}\n            if tool_name:\n                filter_dict[\"tool_name\"] = tool_name\n            if before_date:\n                filter_dict[\"timestamp\"] = {\"$lt\": before_date}\n\n            if filter_dict:\n                # Delete with filter\n                index.delete(filter=filter_dict)\n            else:\n                # Delete all\n                index.delete(delete_all=True)\n\n            # Pinecone doesn't return count of deleted items\n            return 0\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _clear)\n"
  },
  {
    "path": "src/vanna/integrations/plotly/__init__.py",
    "content": "\"\"\"Plotly integration for chart generation.\"\"\"\n\nfrom .chart_generator import PlotlyChartGenerator\n\n__all__ = [\"PlotlyChartGenerator\"]\n"
  },
  {
    "path": "src/vanna/integrations/plotly/chart_generator.py",
    "content": "\"\"\"Plotly-based chart generator with automatic chart type selection.\"\"\"\n\nfrom typing import Dict, Any, List, cast\nimport json\nimport pandas as pd\nimport plotly.graph_objects as go\nimport plotly.express as px\nimport plotly.io as pio\n\n\nclass PlotlyChartGenerator:\n    \"\"\"Generate Plotly charts using heuristics based on DataFrame characteristics.\"\"\"\n\n    # Vanna brand colors from landing page\n    THEME_COLORS = {\n        \"navy\": \"#023d60\",\n        \"cream\": \"#e7e1cf\",\n        \"teal\": \"#15a8a8\",\n        \"orange\": \"#fe5d26\",\n        \"magenta\": \"#bf1363\",\n    }\n\n    # Color palette for charts (excluding cream as it's too light for data)\n    COLOR_PALETTE = [\"#15a8a8\", \"#fe5d26\", \"#bf1363\", \"#023d60\"]\n\n    def generate_chart(self, df: pd.DataFrame, title: str = \"Chart\") -> Dict[str, Any]:\n        \"\"\"Generate a Plotly chart based on DataFrame shape and types.\n\n        Heuristics:\n        - 4+ columns: table\n        - 1 numeric column: histogram\n        - 2 columns (1 categorical, 1 numeric): bar chart\n        - 2 numeric columns: scatter plot\n        - 3+ numeric columns: correlation heatmap or multi-line chart\n        - Time series data: line chart\n        - Multiple categorical: grouped bar chart\n\n        Args:\n            df: DataFrame to visualize\n            title: Title for the chart\n\n        Returns:\n            Plotly figure as dictionary\n\n        Raises:\n            ValueError: If DataFrame is empty or cannot be visualized\n        \"\"\"\n        if df.empty:\n            raise ValueError(\"Cannot visualize empty DataFrame\")\n\n        # Heuristic: If 4 or more columns, render as a table\n        if len(df.columns) >= 4:\n            fig = self._create_table(df, title)\n            result: Dict[str, Any] = json.loads(pio.to_json(fig))\n            return result\n\n        # Identify column types\n        numeric_cols = df.select_dtypes(include=[\"number\"]).columns.tolist()\n        categorical_cols = df.select_dtypes(\n            include=[\"object\", \"category\"]\n        ).columns.tolist()\n        datetime_cols = df.select_dtypes(include=[\"datetime64\"]).columns.tolist()\n\n        # Check for time series\n        is_timeseries = len(datetime_cols) > 0\n\n        # Apply heuristics\n        if is_timeseries and len(numeric_cols) > 0:\n            # Time series line chart\n            fig = self._create_time_series_chart(\n                df, datetime_cols[0], numeric_cols, title\n            )\n        elif len(numeric_cols) == 1 and len(categorical_cols) == 0:\n            # Single numeric column: histogram\n            fig = self._create_histogram(df, numeric_cols[0], title)\n        elif len(numeric_cols) == 1 and len(categorical_cols) == 1:\n            # One categorical, one numeric: bar chart\n            fig = self._create_bar_chart(\n                df, categorical_cols[0], numeric_cols[0], title\n            )\n        elif len(numeric_cols) == 2:\n            # Two numeric columns: scatter plot\n            fig = self._create_scatter_plot(df, numeric_cols[0], numeric_cols[1], title)\n        elif len(numeric_cols) >= 3:\n            # Multiple numeric columns: correlation heatmap\n            fig = self._create_correlation_heatmap(df, numeric_cols, title)\n        elif len(categorical_cols) >= 2:\n            # Multiple categorical: grouped bar chart\n            fig = self._create_grouped_bar_chart(df, categorical_cols, title)\n        else:\n            # Fallback: show first two columns as scatter/bar\n            if len(df.columns) >= 2:\n                fig = self._create_generic_chart(\n                    df, df.columns[0], df.columns[1], title\n                )\n            else:\n                raise ValueError(\n                    \"Cannot determine appropriate visualization for this DataFrame\"\n                )\n\n        # Convert to JSON-serializable dict using plotly's JSON encoder\n        result = json.loads(pio.to_json(fig))\n        return result\n\n    def _apply_standard_layout(self, fig: go.Figure) -> go.Figure:\n        \"\"\"Apply consistent Vanna brand styling to all charts.\n\n        Uses Vanna brand colors from the landing page for a cohesive look.\n\n        Args:\n            fig: Plotly figure to update\n\n        Returns:\n            Updated figure with Vanna brand styling\n        \"\"\"\n        fig.update_layout(\n            # paper_bgcolor='white',\n            # plot_bgcolor='white',\n            font={\"color\": self.THEME_COLORS[\"navy\"]},  # Navy for text\n            autosize=True,  # Allow chart to resize responsively\n            colorway=self.COLOR_PALETTE,  # Use Vanna brand colors for data\n            # Don't set width/height - let frontend handle sizing\n        )\n        return fig\n\n    def _create_histogram(self, df: pd.DataFrame, column: str, title: str) -> go.Figure:\n        \"\"\"Create a histogram for a single numeric column.\"\"\"\n        fig = px.histogram(\n            df,\n            x=column,\n            title=title,\n            color_discrete_sequence=[self.THEME_COLORS[\"teal\"]],\n        )\n        fig.update_layout(xaxis_title=column, yaxis_title=\"Count\", showlegend=False)\n        self._apply_standard_layout(fig)\n        return fig\n\n    def _create_bar_chart(\n        self, df: pd.DataFrame, x_col: str, y_col: str, title: str\n    ) -> go.Figure:\n        \"\"\"Create a bar chart for categorical vs numeric data.\"\"\"\n        # Aggregate if needed\n        agg_df = df.groupby(x_col)[y_col].sum().reset_index()\n        fig = px.bar(\n            agg_df,\n            x=x_col,\n            y=y_col,\n            title=title,\n            color_discrete_sequence=[self.THEME_COLORS[\"orange\"]],\n        )\n        fig.update_layout(xaxis_title=x_col, yaxis_title=y_col)\n        self._apply_standard_layout(fig)\n        return fig\n\n    def _create_scatter_plot(\n        self, df: pd.DataFrame, x_col: str, y_col: str, title: str\n    ) -> go.Figure:\n        \"\"\"Create a scatter plot for two numeric columns.\"\"\"\n        fig = px.scatter(\n            df,\n            x=x_col,\n            y=y_col,\n            title=title,\n            color_discrete_sequence=[self.THEME_COLORS[\"magenta\"]],\n        )\n        fig.update_layout(xaxis_title=x_col, yaxis_title=y_col)\n        self._apply_standard_layout(fig)\n        return fig\n\n    def _create_correlation_heatmap(\n        self, df: pd.DataFrame, columns: List[str], title: str\n    ) -> go.Figure:\n        \"\"\"Create a correlation heatmap for multiple numeric columns.\"\"\"\n        corr_matrix = df[columns].corr()\n        # Custom Vanna color scale: navy (negative) -> cream (neutral) -> teal (positive)\n        vanna_colorscale = [\n            [0.0, self.THEME_COLORS[\"navy\"]],\n            [0.5, self.THEME_COLORS[\"cream\"]],\n            [1.0, self.THEME_COLORS[\"teal\"]],\n        ]\n        fig = cast(\n            go.Figure,\n            px.imshow(\n                corr_matrix,\n                title=title,\n                labels=dict(color=\"Correlation\"),\n                x=columns,\n                y=columns,\n                color_continuous_scale=vanna_colorscale,\n                zmin=-1,\n                zmax=1,\n            ),\n        )\n        self._apply_standard_layout(fig)\n        return fig\n\n    def _create_time_series_chart(\n        self, df: pd.DataFrame, time_col: str, value_cols: List[str], title: str\n    ) -> go.Figure:\n        \"\"\"Create a time series line chart.\"\"\"\n        fig = go.Figure()\n\n        for i, col in enumerate(value_cols[:5]):  # Limit to 5 lines for readability\n            color = self.COLOR_PALETTE[i % len(self.COLOR_PALETTE)]\n            fig.add_trace(\n                go.Scatter(\n                    x=df[time_col],\n                    y=df[col],\n                    mode=\"lines\",\n                    name=col,\n                    line=dict(color=color),\n                )\n            )\n\n        fig.update_layout(\n            title=title,\n            xaxis_title=time_col,\n            yaxis_title=\"Value\",\n            hovermode=\"x unified\",\n        )\n        self._apply_standard_layout(fig)\n        return fig\n\n    def _create_grouped_bar_chart(\n        self, df: pd.DataFrame, categorical_cols: List[str], title: str\n    ) -> go.Figure:\n        \"\"\"Create a grouped bar chart for multiple categorical columns.\"\"\"\n        # Use first two categorical columns\n        if len(categorical_cols) >= 2:\n            # Count occurrences\n            grouped = df.groupby(categorical_cols[:2]).size().reset_index(name=\"count\")\n            fig = px.bar(\n                grouped,\n                x=categorical_cols[0],\n                y=\"count\",\n                color=categorical_cols[1],\n                title=title,\n                barmode=\"group\",\n                color_discrete_sequence=self.COLOR_PALETTE,\n            )\n            self._apply_standard_layout(fig)\n            return fig\n        else:\n            # Single categorical: value counts\n            counts = df[categorical_cols[0]].value_counts().reset_index()\n            counts.columns = [categorical_cols[0], \"count\"]\n            fig = px.bar(\n                counts,\n                x=categorical_cols[0],\n                y=\"count\",\n                title=title,\n                color_discrete_sequence=[self.THEME_COLORS[\"teal\"]],\n            )\n            self._apply_standard_layout(fig)\n            return fig\n\n    def _create_generic_chart(\n        self, df: pd.DataFrame, col1: str, col2: str, title: str\n    ) -> go.Figure:\n        \"\"\"Create a generic chart for any two columns.\"\"\"\n        # Try to determine the best representation\n        if pd.api.types.is_numeric_dtype(df[col1]) and pd.api.types.is_numeric_dtype(\n            df[col2]\n        ):\n            return self._create_scatter_plot(df, col1, col2, title)\n        else:\n            # Treat first as categorical, second as value\n            fig = px.bar(\n                df,\n                x=col1,\n                y=col2,\n                title=title,\n                color_discrete_sequence=[self.THEME_COLORS[\"orange\"]],\n            )\n            self._apply_standard_layout(fig)\n            return fig\n\n    def _create_table(self, df: pd.DataFrame, title: str) -> go.Figure:\n        \"\"\"Create a Plotly table for DataFrames with 4 or more columns.\"\"\"\n        # Prepare header\n        header_values = list(df.columns)\n\n        # Prepare cell values (transpose to get columns)\n        cell_values = [df[col].tolist() for col in df.columns]\n\n        # Create the table\n        fig = go.Figure(\n            data=[\n                go.Table(\n                    header=dict(\n                        values=header_values,\n                        fill_color=self.THEME_COLORS[\"navy\"],\n                        font=dict(color=\"white\", size=12),\n                        align=\"left\",\n                    ),\n                    cells=dict(\n                        values=cell_values,\n                        fill_color=[\n                            [\n                                self.THEME_COLORS[\"cream\"] if i % 2 == 0 else \"white\"\n                                for i in range(len(df))\n                            ]\n                        ],\n                        font=dict(color=self.THEME_COLORS[\"navy\"], size=11),\n                        align=\"left\",\n                    ),\n                )\n            ]\n        )\n\n        fig.update_layout(title=title, font={\"color\": self.THEME_COLORS[\"navy\"]})\n\n        return fig\n"
  },
  {
    "path": "src/vanna/integrations/postgres/__init__.py",
    "content": "\"\"\"\nPostgreSQL integration.\n\nThis module provides PostgreSQL runner implementation.\n\"\"\"\n\nfrom .sql_runner import PostgresRunner\n\n__all__ = [\"PostgresRunner\"]\n"
  },
  {
    "path": "src/vanna/integrations/postgres/sql_runner.py",
    "content": "\"\"\"PostgreSQL implementation of SqlRunner interface.\"\"\"\n\nfrom typing import Optional\nimport pandas as pd\n\nfrom vanna.capabilities.sql_runner import SqlRunner, RunSqlToolArgs\nfrom vanna.core.tool import ToolContext\n\n\nclass PostgresRunner(SqlRunner):\n    \"\"\"PostgreSQL implementation of the SqlRunner interface.\"\"\"\n\n    def __init__(\n        self,\n        connection_string: Optional[str] = None,\n        host: Optional[str] = None,\n        port: Optional[int] = 5432,\n        database: Optional[str] = None,\n        user: Optional[str] = None,\n        password: Optional[str] = None,\n        **kwargs,\n    ):\n        \"\"\"Initialize with PostgreSQL connection parameters.\n\n        You can either provide a connection_string OR individual parameters (host, database, etc.).\n        If connection_string is provided, it takes precedence.\n\n        Args:\n            connection_string: PostgreSQL connection string (e.g., \"postgresql://user:password@host:port/database\")\n            host: Database host address\n            port: Database port (default: 5432)\n            database: Database name\n            user: Database user\n            password: Database password\n            **kwargs: Additional psycopg2 connection parameters (sslmode, connect_timeout, etc.)\n        \"\"\"\n        try:\n            import psycopg2\n            import psycopg2.extras\n\n            self.psycopg2 = psycopg2\n        except Exception as e:\n            raise ImportError(\n                \"psycopg2 package is required. Install with: pip install 'vanna[postgres]'\"\n            ) from e\n\n        if connection_string:\n            self.connection_string = connection_string\n            self.connection_params = None\n        elif host and database and user:\n            self.connection_string = None\n            self.connection_params = {\n                \"host\": host,\n                \"port\": port,\n                \"database\": database,\n                \"user\": user,\n                \"password\": password,\n                **kwargs,\n            }\n        else:\n            raise ValueError(\n                \"Either provide connection_string OR (host, database, and user) parameters\"\n            )\n\n    async def run_sql(self, args: RunSqlToolArgs, context: ToolContext) -> pd.DataFrame:\n        \"\"\"Execute SQL query against PostgreSQL database and return results as DataFrame.\n\n        Args:\n            args: SQL query arguments\n            context: Tool execution context\n\n        Returns:\n            DataFrame with query results\n\n        Raises:\n            psycopg2.Error: If query execution fails\n        \"\"\"\n        # Connect to the database using either connection string or parameters\n        if self.connection_string:\n            conn = self.psycopg2.connect(self.connection_string)\n        else:\n            conn = self.psycopg2.connect(**self.connection_params)\n\n        cursor = conn.cursor(cursor_factory=self.psycopg2.extras.RealDictCursor)\n\n        try:\n            # Execute the query\n            cursor.execute(args.sql)\n\n            # Determine if this is a SELECT query or modification query\n            query_type = args.sql.strip().upper().split()[0]\n\n            if query_type == \"SELECT\":\n                # Fetch results for SELECT queries\n                rows = cursor.fetchall()\n                if not rows:\n                    # Return empty DataFrame\n                    return pd.DataFrame()\n\n                # Convert rows to list of dictionaries\n                results_data = [dict(row) for row in rows]\n                return pd.DataFrame(results_data)\n            else:\n                # For non-SELECT queries (INSERT, UPDATE, DELETE, etc.)\n                conn.commit()\n                rows_affected = cursor.rowcount\n                # Return a DataFrame indicating rows affected\n                return pd.DataFrame({\"rows_affected\": [rows_affected]})\n\n        finally:\n            cursor.close()\n            conn.close()\n"
  },
  {
    "path": "src/vanna/integrations/premium/agent_memory/__init__.py",
    "content": "\"\"\"\nCloud-based agent memory implementations.\n\"\"\"\n\nfrom .premium import CloudAgentMemory\n\n__all__ = [\"CloudAgentMemory\"]\n"
  },
  {
    "path": "src/vanna/integrations/premium/agent_memory/premium.py",
    "content": "\"\"\"\nCloud-based implementation of AgentMemory.\n\nThis implementation uses Vanna's premium cloud service for storing and searching\ntool usage patterns with advanced similarity search and analytics.\n\"\"\"\n\nimport json\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional\nimport httpx\n\nfrom vanna.capabilities.agent_memory import (\n    AgentMemory,\n    TextMemory,\n    TextMemorySearchResult,\n    ToolMemory,\n    ToolMemorySearchResult,\n)\nfrom vanna.core.tool import ToolContext\n\n\nclass CloudAgentMemory(AgentMemory):\n    \"\"\"Cloud-based implementation of AgentMemory.\"\"\"\n\n    def __init__(\n        self,\n        api_base_url: str = \"https://api.vanna.ai\",\n        api_key: Optional[str] = None,\n        organization_id: Optional[str] = None,\n    ):\n        self.api_base_url = api_base_url.rstrip(\"/\")\n        self.api_key = api_key\n        self.organization_id = organization_id\n        self._client = httpx.AsyncClient(base_url=self.api_base_url, timeout=30.0)\n\n    def _get_headers(self) -> Dict[str, str]:\n        \"\"\"Get request headers with authentication.\"\"\"\n        headers = {\"Content-Type\": \"application/json\"}\n        if self.api_key:\n            headers[\"Authorization\"] = f\"Bearer {self.api_key}\"\n        if self.organization_id:\n            headers[\"X-Organization-ID\"] = self.organization_id\n        return headers\n\n    async def save_tool_usage(\n        self,\n        question: str,\n        tool_name: str,\n        args: Dict[str, Any],\n        context: ToolContext,\n        success: bool = True,\n        metadata: Optional[Dict[str, Any]] = None,\n    ) -> None:\n        \"\"\"Save a tool usage pattern to premium cloud storage.\"\"\"\n        import uuid\n\n        payload = {\n            \"id\": str(uuid.uuid4()),\n            \"question\": question,\n            \"tool_name\": tool_name,\n            \"args\": args,\n            \"success\": success,\n            \"metadata\": metadata or {},\n            \"timestamp\": datetime.now().isoformat(),\n        }\n\n        response = await self._client.post(\n            \"/memory/tool-usage\", json=payload, headers=self._get_headers()\n        )\n        response.raise_for_status()\n\n    async def search_similar_usage(\n        self,\n        question: str,\n        context: ToolContext,\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n        tool_name_filter: Optional[str] = None,\n    ) -> List[ToolMemorySearchResult]:\n        \"\"\"Search for similar tool usage patterns in premium cloud storage.\"\"\"\n        params = {\n            \"question\": question,\n            \"limit\": limit,\n            \"similarity_threshold\": similarity_threshold,\n        }\n        if tool_name_filter:\n            params[\"tool_name_filter\"] = tool_name_filter\n\n        response = await self._client.get(\n            \"/memory/search-similar\", params=params, headers=self._get_headers()\n        )\n        response.raise_for_status()\n\n        data = response.json()\n        results = []\n\n        for item in data.get(\"results\", []):\n            memory = ToolMemory(**item[\"memory\"])\n            result = ToolMemorySearchResult(\n                memory=memory,\n                similarity_score=item[\"similarity_score\"],\n                rank=item[\"rank\"],\n            )\n            results.append(result)\n\n        return results\n\n    async def get_recent_memories(\n        self, context: ToolContext, limit: int = 10\n    ) -> List[ToolMemory]:\n        \"\"\"Get recently added memories from premium cloud storage.\"\"\"\n        params = {\"limit\": limit}\n\n        response = await self._client.get(\n            \"/memory/recent\", params=params, headers=self._get_headers()\n        )\n        response.raise_for_status()\n\n        data = response.json()\n        memories = []\n\n        for item in data.get(\"memories\", []):\n            memory = ToolMemory(**item)\n            memories.append(memory)\n\n        return memories\n\n    async def delete_by_id(self, context: ToolContext, memory_id: str) -> bool:\n        \"\"\"Delete a memory by its ID from premium cloud storage.\"\"\"\n        response = await self._client.delete(\n            f\"/memory/{memory_id}\", headers=self._get_headers()\n        )\n\n        if response.status_code == 404:\n            return False\n\n        response.raise_for_status()\n        return True\n\n    async def save_text_memory(self, content: str, context: ToolContext) -> TextMemory:\n        \"\"\"Cloud implementation does not yet support text memories.\"\"\"\n        raise NotImplementedError(\"CloudAgentMemory does not support text memories.\")\n\n    async def search_text_memories(\n        self,\n        query: str,\n        context: ToolContext,\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n    ) -> List[TextMemorySearchResult]:\n        \"\"\"Cloud implementation does not yet support text memories.\"\"\"\n        return []\n\n    async def get_recent_text_memories(\n        self, context: ToolContext, limit: int = 10\n    ) -> List[TextMemory]:\n        \"\"\"Cloud implementation does not yet support text memories.\"\"\"\n        return []\n\n    async def delete_text_memory(self, context: ToolContext, memory_id: str) -> bool:\n        \"\"\"Cloud implementation does not yet support text memories.\"\"\"\n        return False\n\n    async def clear_memories(\n        self,\n        context: ToolContext,\n        tool_name: Optional[str] = None,\n        before_date: Optional[str] = None,\n    ) -> int:\n        \"\"\"Clear stored memories from premium cloud storage.\"\"\"\n        payload = {}\n        if tool_name:\n            payload[\"tool_name\"] = tool_name\n        if before_date:\n            payload[\"before_date\"] = before_date\n\n        response = await self._client.delete(\n            \"/memory/clear\", json=payload, headers=self._get_headers()\n        )\n        response.raise_for_status()\n\n        data = response.json()\n        return data.get(\"deleted_count\", 0)\n"
  },
  {
    "path": "src/vanna/integrations/presto/__init__.py",
    "content": "\"\"\"Presto integration for Vanna.\"\"\"\n\nfrom .sql_runner import PrestoRunner\n\n__all__ = [\"PrestoRunner\"]\n"
  },
  {
    "path": "src/vanna/integrations/presto/sql_runner.py",
    "content": "\"\"\"Presto implementation of SqlRunner interface.\"\"\"\n\nfrom typing import Optional\nimport pandas as pd\n\nfrom vanna.capabilities.sql_runner import SqlRunner, RunSqlToolArgs\nfrom vanna.core.tool import ToolContext\n\n\nclass PrestoRunner(SqlRunner):\n    \"\"\"Presto implementation of the SqlRunner interface.\"\"\"\n\n    def __init__(\n        self,\n        host: str,\n        catalog: str = \"hive\",\n        schema: str = \"default\",\n        user: Optional[str] = None,\n        password: Optional[str] = None,\n        port: int = 443,\n        combined_pem_path: Optional[str] = None,\n        protocol: str = \"https\",\n        requests_kwargs: Optional[dict] = None,\n        **kwargs,\n    ):\n        \"\"\"Initialize with Presto connection parameters.\n\n        Args:\n            host: The host address of the Presto database\n            catalog: The catalog to use in the Presto environment (default: 'hive')\n            schema: The schema to use in the Presto environment (default: 'default')\n            user: The username for authentication\n            password: The password for authentication\n            port: The port number for the Presto connection (default: 443)\n            combined_pem_path: The path to the combined pem file for SSL connection\n            protocol: The protocol to use for the connection (default: 'https')\n            requests_kwargs: Additional keyword arguments for requests\n            **kwargs: Additional pyhive connection parameters\n        \"\"\"\n        try:\n            from pyhive import presto\n\n            self.presto = presto\n        except ImportError as e:\n            raise ImportError(\n                \"pyhive package is required. Install with: pip install pyhive\"\n            ) from e\n\n        self.host = host\n        self.catalog = catalog\n        self.schema = schema\n        self.user = user\n        self.password = password\n        self.port = port\n        self.protocol = protocol\n        self.kwargs = kwargs\n\n        # Set up requests_kwargs for SSL if combined_pem_path is provided\n        if requests_kwargs is None and combined_pem_path is not None:\n            self.requests_kwargs = {\"verify\": combined_pem_path}\n        else:\n            self.requests_kwargs = requests_kwargs\n\n    async def run_sql(self, args: RunSqlToolArgs, context: ToolContext) -> pd.DataFrame:\n        \"\"\"Execute SQL query against Presto database and return results as DataFrame.\n\n        Args:\n            args: SQL query arguments\n            context: Tool execution context\n\n        Returns:\n            DataFrame with query results\n\n        Raises:\n            presto.Error: If query execution fails\n        \"\"\"\n        # Connect to the database\n        conn = self.presto.Connection(\n            host=self.host,\n            username=self.user,\n            password=self.password,\n            catalog=self.catalog,\n            schema=self.schema,\n            port=self.port,\n            protocol=self.protocol,\n            requests_kwargs=self.requests_kwargs,\n            **self.kwargs,\n        )\n\n        try:\n            # Strip and remove trailing semicolons (Presto doesn't like them)\n            sql = args.sql.rstrip()\n            if sql.endswith(\";\"):\n                sql = sql[:-1]\n\n            cursor = conn.cursor()\n            cursor.execute(sql)\n            results = cursor.fetchall()\n\n            # Create a pandas dataframe from the results\n            df = pd.DataFrame(results, columns=[desc[0] for desc in cursor.description])\n\n            cursor.close()\n            return df\n\n        finally:\n            conn.close()\n"
  },
  {
    "path": "src/vanna/integrations/qdrant/__init__.py",
    "content": "\"\"\"\nQdrant integration for Vanna Agents.\n\"\"\"\n\nfrom .agent_memory import QdrantAgentMemory\n\n__all__ = [\"QdrantAgentMemory\"]\n"
  },
  {
    "path": "src/vanna/integrations/qdrant/agent_memory.py",
    "content": "\"\"\"\nQdrant vector database implementation of AgentMemory.\n\nThis implementation uses Qdrant for vector storage of tool usage patterns.\n\"\"\"\n\nimport json\nimport uuid\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional\nimport asyncio\nfrom concurrent.futures import ThreadPoolExecutor\n\ntry:\n    from qdrant_client import QdrantClient\n    from qdrant_client.models import (\n        Distance,\n        VectorParams,\n        PointStruct,\n        Filter,\n        FieldCondition,\n        MatchValue,\n    )\n\n    QDRANT_AVAILABLE = True\nexcept ImportError:\n    QDRANT_AVAILABLE = False\n\nfrom vanna.capabilities.agent_memory import (\n    AgentMemory,\n    TextMemory,\n    TextMemorySearchResult,\n    ToolMemory,\n    ToolMemorySearchResult,\n)\nfrom vanna.core.tool import ToolContext\n\n\nclass QdrantAgentMemory(AgentMemory):\n    \"\"\"Qdrant-based implementation of AgentMemory.\"\"\"\n\n    def __init__(\n        self,\n        collection_name: str = \"tool_memories\",\n        url: Optional[str] = None,\n        path: Optional[str] = None,\n        api_key: Optional[str] = None,\n        dimension: int = 384,\n    ):\n        if not QDRANT_AVAILABLE:\n            raise ImportError(\n                \"Qdrant is required for QdrantAgentMemory. Install with: pip install qdrant-client\"\n            )\n\n        self.collection_name = collection_name\n        self.url = url\n        self.path = path\n        self.api_key = api_key\n        self.dimension = dimension\n        self._client = None\n        self._executor = ThreadPoolExecutor(max_workers=2)\n\n    def _get_client(self):\n        \"\"\"Get or create Qdrant client.\"\"\"\n        if self._client is None:\n            if self.url:\n                self._client = QdrantClient(url=self.url, api_key=self.api_key)\n            else:\n                self._client = QdrantClient(path=self.path or \":memory:\")\n\n            # Create collection if it doesn't exist\n            collections = self._client.get_collections().collections\n            if not any(c.name == self.collection_name for c in collections):\n                self._client.create_collection(\n                    collection_name=self.collection_name,\n                    vectors_config=VectorParams(\n                        size=self.dimension, distance=Distance.COSINE\n                    ),\n                )\n        return self._client\n\n    def _create_embedding(self, text: str) -> List[float]:\n        \"\"\"Create a simple embedding from text (placeholder).\"\"\"\n        import hashlib\n\n        hash_val = int(hashlib.md5(text.encode()).hexdigest(), 16)\n        return [(hash_val >> i) % 100 / 100.0 for i in range(self.dimension)]\n\n    async def save_tool_usage(\n        self,\n        question: str,\n        tool_name: str,\n        args: Dict[str, Any],\n        context: ToolContext,\n        success: bool = True,\n        metadata: Optional[Dict[str, Any]] = None,\n    ) -> None:\n        \"\"\"Save a tool usage pattern.\"\"\"\n\n        def _save():\n            client = self._get_client()\n\n            memory_id = str(uuid.uuid4())\n            timestamp = datetime.now().isoformat()\n            embedding = self._create_embedding(question)\n\n            payload = {\n                \"question\": question,\n                \"tool_name\": tool_name,\n                \"args\": args,\n                \"timestamp\": timestamp,\n                \"success\": success,\n                \"metadata\": metadata or {},\n            }\n\n            point = PointStruct(id=memory_id, vector=embedding, payload=payload)\n\n            client.upsert(collection_name=self.collection_name, points=[point])\n\n        await asyncio.get_event_loop().run_in_executor(self._executor, _save)\n\n    async def search_similar_usage(\n        self,\n        question: str,\n        context: ToolContext,\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n        tool_name_filter: Optional[str] = None,\n    ) -> List[ToolMemorySearchResult]:\n        \"\"\"Search for similar tool usage patterns.\"\"\"\n\n        def _search():\n            client = self._get_client()\n\n            embedding = self._create_embedding(question)\n\n            # Build filter\n            query_filter = None\n            conditions = [FieldCondition(key=\"success\", match=MatchValue(value=True))]\n            if tool_name_filter:\n                conditions.append(\n                    FieldCondition(\n                        key=\"tool_name\", match=MatchValue(value=tool_name_filter)\n                    )\n                )\n\n            if conditions:\n                query_filter = Filter(must=conditions)\n\n            # Use query_points for newer qdrant-client (1.8.0+) or search for older versions\n            if hasattr(client, \"query_points\"):\n                results = client.query_points(\n                    collection_name=self.collection_name,\n                    query=embedding,\n                    limit=limit,\n                    query_filter=query_filter,\n                    score_threshold=similarity_threshold,\n                ).points\n            else:\n                # Fallback to search method for older qdrant-client versions\n                results = client.search(\n                    collection_name=self.collection_name,\n                    query_vector=embedding,\n                    limit=limit,\n                    query_filter=query_filter,\n                    score_threshold=similarity_threshold,\n                )\n\n            search_results = []\n            for i, hit in enumerate(results):\n                payload = hit.payload\n\n                memory = ToolMemory(\n                    memory_id=str(hit.id),\n                    question=payload[\"question\"],\n                    tool_name=payload[\"tool_name\"],\n                    args=payload[\"args\"],\n                    timestamp=payload.get(\"timestamp\"),\n                    success=payload.get(\"success\", True),\n                    metadata=payload.get(\"metadata\", {}),\n                )\n\n                search_results.append(\n                    ToolMemorySearchResult(\n                        memory=memory, similarity_score=hit.score, rank=i + 1\n                    )\n                )\n\n            return search_results\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _search)\n\n    async def get_recent_memories(\n        self, context: ToolContext, limit: int = 10\n    ) -> List[ToolMemory]:\n        \"\"\"Get recently added memories.\"\"\"\n\n        def _get_recent():\n            client = self._get_client()\n\n            # Scroll through all points and sort by timestamp\n            points, _ = client.scroll(\n                collection_name=self.collection_name,\n                limit=1000,  # Get more than we need to sort\n                with_payload=True,\n                with_vectors=False,\n            )\n\n            # Sort by timestamp\n            sorted_points = sorted(\n                points, key=lambda p: p.payload.get(\"timestamp\", \"\"), reverse=True\n            )\n\n            memories = []\n            for point in sorted_points[:limit]:\n                payload = point.payload\n\n                # Skip text memories - they have is_text_memory flag\n                if payload.get(\"is_text_memory\"):\n                    continue\n\n                memory = ToolMemory(\n                    memory_id=str(point.id),\n                    question=payload[\"question\"],\n                    tool_name=payload[\"tool_name\"],\n                    args=payload[\"args\"],\n                    timestamp=payload.get(\"timestamp\"),\n                    success=payload.get(\"success\", True),\n                    metadata=payload.get(\"metadata\", {}),\n                )\n                memories.append(memory)\n\n            return memories\n\n        return await asyncio.get_event_loop().run_in_executor(\n            self._executor, _get_recent\n        )\n\n    async def delete_by_id(self, context: ToolContext, memory_id: str) -> bool:\n        \"\"\"Delete a memory by its ID. Returns True if deleted, False if not found.\"\"\"\n\n        def _delete():\n            client = self._get_client()\n\n            try:\n                # Check if the point exists before attempting to delete\n                points = client.retrieve(\n                    collection_name=self.collection_name,\n                    ids=[memory_id],\n                    with_payload=False,\n                    with_vectors=False,\n                )\n\n                if points and len(points) > 0:\n                    client.delete(\n                        collection_name=self.collection_name,\n                        points_selector=[memory_id],\n                    )\n                    return True\n                return False\n            except Exception:\n                return False\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _delete)\n\n    async def save_text_memory(self, content: str, context: ToolContext) -> TextMemory:\n        \"\"\"Save a text memory.\"\"\"\n\n        def _save():\n            client = self._get_client()\n\n            memory_id = str(uuid.uuid4())\n            timestamp = datetime.now().isoformat()\n            embedding = self._create_embedding(content)\n\n            payload = {\n                \"content\": content,\n                \"timestamp\": timestamp,\n                \"is_text_memory\": True,\n            }\n\n            point = PointStruct(id=memory_id, vector=embedding, payload=payload)\n\n            client.upsert(collection_name=self.collection_name, points=[point])\n\n            return TextMemory(memory_id=memory_id, content=content, timestamp=timestamp)\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _save)\n\n    async def search_text_memories(\n        self,\n        query: str,\n        context: ToolContext,\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n    ) -> List[TextMemorySearchResult]:\n        \"\"\"Search for similar text memories.\"\"\"\n\n        def _search():\n            client = self._get_client()\n\n            embedding = self._create_embedding(query)\n\n            query_filter = Filter(\n                must=[\n                    FieldCondition(key=\"is_text_memory\", match=MatchValue(value=True))\n                ]\n            )\n\n            # Use query_points for newer qdrant-client (1.8.0+) or search for older versions\n            if hasattr(client, \"query_points\"):\n                results = client.query_points(\n                    collection_name=self.collection_name,\n                    query=embedding,\n                    limit=limit,\n                    query_filter=query_filter,\n                    score_threshold=similarity_threshold,\n                ).points\n            else:\n                # Fallback to search method for older qdrant-client versions\n                results = client.search(\n                    collection_name=self.collection_name,\n                    query_vector=embedding,\n                    limit=limit,\n                    query_filter=query_filter,\n                    score_threshold=similarity_threshold,\n                )\n\n            search_results = []\n            for i, hit in enumerate(results):\n                payload = hit.payload\n\n                memory = TextMemory(\n                    memory_id=str(hit.id),\n                    content=payload.get(\"content\", \"\"),\n                    timestamp=payload.get(\"timestamp\"),\n                )\n\n                search_results.append(\n                    TextMemorySearchResult(\n                        memory=memory, similarity_score=hit.score, rank=i + 1\n                    )\n                )\n\n            return search_results\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _search)\n\n    async def get_recent_text_memories(\n        self, context: ToolContext, limit: int = 10\n    ) -> List[TextMemory]:\n        \"\"\"Get recently added text memories.\"\"\"\n\n        def _get_recent():\n            client = self._get_client()\n\n            # Scroll through text memory points and sort by timestamp\n            points, _ = client.scroll(\n                collection_name=self.collection_name,\n                scroll_filter=Filter(\n                    must=[\n                        FieldCondition(\n                            key=\"is_text_memory\", match=MatchValue(value=True)\n                        )\n                    ]\n                ),\n                limit=1000,\n                with_payload=True,\n                with_vectors=False,\n            )\n\n            # Sort by timestamp\n            sorted_points = sorted(\n                points, key=lambda p: p.payload.get(\"timestamp\", \"\"), reverse=True\n            )\n\n            memories = []\n            for point in sorted_points[:limit]:\n                payload = point.payload\n                memory = TextMemory(\n                    memory_id=str(point.id),\n                    content=payload.get(\"content\", \"\"),\n                    timestamp=payload.get(\"timestamp\"),\n                )\n                memories.append(memory)\n\n            return memories\n\n        return await asyncio.get_event_loop().run_in_executor(\n            self._executor, _get_recent\n        )\n\n    async def delete_text_memory(self, context: ToolContext, memory_id: str) -> bool:\n        \"\"\"Delete a text memory by its ID.\"\"\"\n\n        def _delete():\n            client = self._get_client()\n\n            try:\n                # Check if the point exists before attempting to delete\n                points = client.retrieve(\n                    collection_name=self.collection_name,\n                    ids=[memory_id],\n                    with_payload=False,\n                    with_vectors=False,\n                )\n\n                if points and len(points) > 0:\n                    client.delete(\n                        collection_name=self.collection_name,\n                        points_selector=[memory_id],\n                    )\n                    return True\n                return False\n            except Exception:\n                return False\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _delete)\n\n    async def clear_memories(\n        self,\n        context: ToolContext,\n        tool_name: Optional[str] = None,\n        before_date: Optional[str] = None,\n    ) -> int:\n        \"\"\"Clear stored memories.\"\"\"\n\n        def _clear():\n            client = self._get_client()\n\n            # Build filter\n            conditions = []\n            if tool_name:\n                conditions.append(\n                    FieldCondition(key=\"tool_name\", match=MatchValue(value=tool_name))\n                )\n            if before_date:\n                conditions.append(\n                    FieldCondition(key=\"timestamp\", match=MatchValue(value=before_date))\n                )\n\n            if conditions or (tool_name is None and before_date is None):\n                # Delete with filter or delete all\n                query_filter = Filter(must=conditions) if conditions else None\n\n                if query_filter:\n                    client.delete(\n                        collection_name=self.collection_name,\n                        points_selector=query_filter,\n                    )\n                else:\n                    # Delete all points\n                    client.delete_collection(collection_name=self.collection_name)\n                    # Recreate empty collection\n                    client.create_collection(\n                        collection_name=self.collection_name,\n                        vectors_config=VectorParams(\n                            size=self.dimension, distance=Distance.COSINE\n                        ),\n                    )\n\n            return 0  # Qdrant doesn't return count\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _clear)\n"
  },
  {
    "path": "src/vanna/integrations/snowflake/__init__.py",
    "content": "\"\"\"Snowflake integration for Vanna.\"\"\"\n\nfrom .sql_runner import SnowflakeRunner\n\n__all__ = [\"SnowflakeRunner\"]\n"
  },
  {
    "path": "src/vanna/integrations/snowflake/sql_runner.py",
    "content": "\"\"\"Snowflake implementation of SqlRunner interface.\"\"\"\n\nfrom typing import Optional, Union\nimport os\nimport pandas as pd\n\nfrom vanna.capabilities.sql_runner import SqlRunner, RunSqlToolArgs\nfrom vanna.core.tool import ToolContext\n\n\nclass SnowflakeRunner(SqlRunner):\n    \"\"\"Snowflake implementation of the SqlRunner interface.\"\"\"\n\n    def __init__(\n        self,\n        account: str,\n        username: str,\n        password: Optional[str] = None,\n        database: str = \"\",\n        role: Optional[str] = None,\n        warehouse: Optional[str] = None,\n        private_key_path: Optional[str] = None,\n        private_key_passphrase: Optional[str] = None,\n        private_key_content: Optional[bytes] = None,\n        **kwargs,\n    ):\n        \"\"\"Initialize with Snowflake connection parameters.\n\n        Args:\n            account: Snowflake account identifier\n            username: Database user\n            password: Database password (optional if using key-pair auth)\n            database: Database name\n            role: Snowflake role to use (optional)\n            warehouse: Snowflake warehouse to use (optional)\n            private_key_path: Path to private key file for RSA key-pair authentication (optional)\n            private_key_passphrase: Passphrase for encrypted private key (optional)\n            private_key_content: Private key content as bytes (optional, alternative to private_key_path)\n            **kwargs: Additional snowflake.connector connection parameters\n\n        Note:\n            Either password OR private_key_path/private_key_content must be provided.\n            RSA key-pair authentication is recommended for production systems as Snowflake\n            is deprecating user/password authentication.\n        \"\"\"\n        try:\n            import snowflake.connector\n\n            self.snowflake = snowflake.connector\n        except ImportError as e:\n            raise ImportError(\n                \"snowflake-connector-python package is required. \"\n                \"Install with: pip install 'vanna[snowflake]'\"\n            ) from e\n\n        # Validate that at least one authentication method is provided\n        if not password and not private_key_path and not private_key_content:\n            raise ValueError(\n                \"Either password or private_key_path/private_key_content must be provided for authentication\"\n            )\n\n        # Validate private key path exists if provided\n        if private_key_path and not os.path.isfile(private_key_path):\n            raise FileNotFoundError(f\"Private key file not found: {private_key_path}\")\n\n        self.account = account\n        self.username = username\n        self.password = password\n        self.database = database\n        self.role = role\n        self.warehouse = warehouse\n        self.private_key_path = private_key_path\n        self.private_key_passphrase = private_key_passphrase\n        self.private_key_content = private_key_content\n        self.kwargs = kwargs\n\n    async def run_sql(self, args: RunSqlToolArgs, context: ToolContext) -> pd.DataFrame:\n        \"\"\"Execute SQL query against Snowflake database and return results as DataFrame.\n\n        Args:\n            args: SQL query arguments\n            context: Tool execution context\n\n        Returns:\n            DataFrame with query results\n\n        Raises:\n            snowflake.connector.Error: If query execution fails\n        \"\"\"\n        # Build connection parameters\n        conn_params = {\n            \"user\": self.username,\n            \"account\": self.account,\n            \"client_session_keep_alive\": True,\n        }\n\n        # Add database if specified\n        if self.database:\n            conn_params[\"database\"] = self.database\n\n        # Configure authentication method\n        if self.private_key_path or self.private_key_content:\n            # Use RSA key-pair authentication\n            if self.private_key_path:\n                conn_params[\"private_key_path\"] = self.private_key_path\n            else:\n                conn_params[\"private_key_content\"] = self.private_key_content\n\n            # Add passphrase if provided\n            if self.private_key_passphrase:\n                conn_params[\"private_key_passphrase\"] = self.private_key_passphrase\n        else:\n            # Use password authentication (fallback)\n            conn_params[\"password\"] = self.password\n\n        # Add any additional kwargs\n        conn_params.update(self.kwargs)\n\n        # Connect to the database\n        conn = self.snowflake.connect(**conn_params)\n\n        cursor = conn.cursor()\n\n        try:\n            # Set role if specified\n            if self.role:\n                cursor.execute(f\"USE ROLE {self.role}\")\n\n            # Set warehouse if specified\n            if self.warehouse:\n                cursor.execute(f\"USE WAREHOUSE {self.warehouse}\")\n\n            # Use the specified database if provided\n            if self.database:\n                cursor.execute(f\"USE DATABASE {self.database}\")\n\n            # Execute the query\n            cursor.execute(args.sql)\n            results = cursor.fetchall()\n\n            # Create a pandas dataframe from the results\n            df = pd.DataFrame(results, columns=[desc[0] for desc in cursor.description])\n            return df\n\n        finally:\n            cursor.close()\n            conn.close()\n"
  },
  {
    "path": "src/vanna/integrations/sqlite/__init__.py",
    "content": "\"\"\"\nSQLite integration.\n\nThis module provides SQLite runner implementation.\n\"\"\"\n\nfrom .sql_runner import SqliteRunner\n\n__all__ = [\"SqliteRunner\"]\n"
  },
  {
    "path": "src/vanna/integrations/sqlite/sql_runner.py",
    "content": "\"\"\"SQLite implementation of SqlRunner interface.\"\"\"\n\nimport sqlite3\nimport pandas as pd\n\nfrom vanna.capabilities.sql_runner import SqlRunner, RunSqlToolArgs\nfrom vanna.core.tool import ToolContext\n\n\nclass SqliteRunner(SqlRunner):\n    \"\"\"SQLite implementation of the SqlRunner interface.\"\"\"\n\n    def __init__(self, database_path: str):\n        \"\"\"Initialize with a SQLite database path.\n\n        Args:\n            database_path: Path to the SQLite database file\n        \"\"\"\n        self.database_path = database_path\n\n    async def run_sql(self, args: RunSqlToolArgs, context: ToolContext) -> pd.DataFrame:\n        \"\"\"Execute SQL query against SQLite database and return results as DataFrame.\n\n        Args:\n            args: SQL query arguments\n            context: Tool execution context\n\n        Returns:\n            DataFrame with query results\n\n        Raises:\n            sqlite3.Error: If query execution fails\n        \"\"\"\n        # Connect to the database\n        conn = sqlite3.connect(self.database_path)\n        conn.row_factory = sqlite3.Row  # Enable column access by name\n        cursor = conn.cursor()\n\n        try:\n            # Execute the query\n            cursor.execute(args.sql)\n\n            # Determine if this is a SELECT query or modification query\n            query_type = args.sql.strip().upper().split()[0]\n\n            if query_type == \"SELECT\":\n                # Fetch results for SELECT queries\n                rows = cursor.fetchall()\n                if not rows:\n                    # Return empty DataFrame\n                    return pd.DataFrame()\n\n                # Convert rows to list of dictionaries\n                results_data = [dict(row) for row in rows]\n                return pd.DataFrame(results_data)\n            else:\n                # For non-SELECT queries (INSERT, UPDATE, DELETE, etc.)\n                conn.commit()\n                rows_affected = cursor.rowcount\n                # Return a DataFrame indicating rows affected\n                return pd.DataFrame({\"rows_affected\": [rows_affected]})\n\n        finally:\n            cursor.close()\n            conn.close()\n"
  },
  {
    "path": "src/vanna/integrations/weaviate/__init__.py",
    "content": "\"\"\"\nWeaviate integration for Vanna Agents.\n\"\"\"\n\nfrom .agent_memory import WeaviateAgentMemory\n\n__all__ = [\"WeaviateAgentMemory\"]\n"
  },
  {
    "path": "src/vanna/integrations/weaviate/agent_memory.py",
    "content": "\"\"\"\nWeaviate vector database implementation of AgentMemory.\n\nThis implementation uses Weaviate for semantic search and storage of tool usage patterns.\n\"\"\"\n\nimport json\nimport uuid\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional\nimport asyncio\nfrom concurrent.futures import ThreadPoolExecutor\n\ntry:\n    import weaviate\n    from weaviate.classes.config import (\n        Configure,\n        Property,\n        DataType as WeaviateDataType,\n    )\n\n    WEAVIATE_AVAILABLE = True\nexcept ImportError:\n    WEAVIATE_AVAILABLE = False\n\nfrom vanna.capabilities.agent_memory import (\n    AgentMemory,\n    TextMemory,\n    TextMemorySearchResult,\n    ToolMemory,\n    ToolMemorySearchResult,\n)\nfrom vanna.core.tool import ToolContext\n\n\nclass WeaviateAgentMemory(AgentMemory):\n    \"\"\"Weaviate-based implementation of AgentMemory.\"\"\"\n\n    def __init__(\n        self,\n        collection_name: str = \"ToolMemory\",\n        url: str = \"http://localhost:8080\",\n        api_key: Optional[str] = None,\n        dimension: int = 384,\n    ):\n        if not WEAVIATE_AVAILABLE:\n            raise ImportError(\n                \"Weaviate is required for WeaviateAgentMemory. Install with: pip install weaviate-client\"\n            )\n\n        self.collection_name = collection_name\n        self.url = url\n        self.api_key = api_key\n        self.dimension = dimension\n        self._client = None\n        self._executor = ThreadPoolExecutor(max_workers=2)\n\n    def _get_client(self):\n        \"\"\"Get or create Weaviate client.\"\"\"\n        if self._client is None:\n            if self.api_key:\n                self._client = weaviate.connect_to_weaviate_cloud(\n                    cluster_url=self.url,\n                    auth_credentials=weaviate.auth.AuthApiKey(self.api_key),\n                )\n            else:\n                self._client = weaviate.connect_to_local(\n                    host=self.url.replace(\"http://\", \"\").replace(\"https://\", \"\")\n                )\n\n            # Create collection if it doesn't exist\n            if not self._client.collections.exists(self.collection_name):\n                self._client.collections.create(\n                    name=self.collection_name,\n                    vectorizer_config=Configure.Vectorizer.none(),\n                    properties=[\n                        Property(name=\"question\", data_type=WeaviateDataType.TEXT),\n                        Property(name=\"tool_name\", data_type=WeaviateDataType.TEXT),\n                        Property(name=\"args_json\", data_type=WeaviateDataType.TEXT),\n                        Property(name=\"timestamp\", data_type=WeaviateDataType.TEXT),\n                        Property(name=\"success\", data_type=WeaviateDataType.BOOL),\n                        Property(name=\"metadata_json\", data_type=WeaviateDataType.TEXT),\n                    ],\n                )\n\n        return self._client\n\n    def _create_embedding(self, text: str) -> List[float]:\n        \"\"\"Create a simple embedding from text (placeholder).\"\"\"\n        import hashlib\n\n        hash_val = int(hashlib.md5(text.encode()).hexdigest(), 16)\n        return [(hash_val >> i) % 100 / 100.0 for i in range(self.dimension)]\n\n    async def save_tool_usage(\n        self,\n        question: str,\n        tool_name: str,\n        args: Dict[str, Any],\n        context: ToolContext,\n        success: bool = True,\n        metadata: Optional[Dict[str, Any]] = None,\n    ) -> None:\n        \"\"\"Save a tool usage pattern.\"\"\"\n\n        def _save():\n            client = self._get_client()\n            collection = client.collections.get(self.collection_name)\n\n            memory_id = str(uuid.uuid4())\n            timestamp = datetime.now().isoformat()\n            embedding = self._create_embedding(question)\n\n            properties = {\n                \"question\": question,\n                \"tool_name\": tool_name,\n                \"args_json\": json.dumps(args),\n                \"timestamp\": timestamp,\n                \"success\": success,\n                \"metadata_json\": json.dumps(metadata or {}),\n            }\n\n            collection.data.insert(\n                properties=properties, vector=embedding, uuid=memory_id\n            )\n\n        await asyncio.get_event_loop().run_in_executor(self._executor, _save)\n\n    async def search_similar_usage(\n        self,\n        question: str,\n        context: ToolContext,\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n        tool_name_filter: Optional[str] = None,\n    ) -> List[ToolMemorySearchResult]:\n        \"\"\"Search for similar tool usage patterns.\"\"\"\n\n        def _search():\n            client = self._get_client()\n            collection = client.collections.get(self.collection_name)\n\n            embedding = self._create_embedding(question)\n\n            # Build filter\n            filters = weaviate.classes.query.Filter.by_property(\"success\").equal(True)\n            if tool_name_filter:\n                filters = filters & weaviate.classes.query.Filter.by_property(\n                    \"tool_name\"\n                ).equal(tool_name_filter)\n\n            response = collection.query.near_vector(\n                near_vector=embedding,\n                limit=limit,\n                filters=filters,\n                return_metadata=weaviate.classes.query.MetadataQuery(distance=True),\n            )\n\n            search_results = []\n            for i, obj in enumerate(response.objects):\n                # Weaviate returns distance, convert to similarity\n                distance = obj.metadata.distance if obj.metadata else 1.0\n                similarity_score = 1 - distance\n\n                if similarity_score >= similarity_threshold:\n                    properties = obj.properties\n                    args = json.loads(properties.get(\"args_json\", \"{}\"))\n                    metadata_dict = json.loads(properties.get(\"metadata_json\", \"{}\"))\n\n                    memory = ToolMemory(\n                        memory_id=str(obj.uuid),\n                        question=properties.get(\"question\"),\n                        tool_name=properties.get(\"tool_name\"),\n                        args=args,\n                        timestamp=properties.get(\"timestamp\"),\n                        success=properties.get(\"success\", True),\n                        metadata=metadata_dict,\n                    )\n\n                    search_results.append(\n                        ToolMemorySearchResult(\n                            memory=memory, similarity_score=similarity_score, rank=i + 1\n                        )\n                    )\n\n            return search_results\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _search)\n\n    async def get_recent_memories(\n        self, context: ToolContext, limit: int = 10\n    ) -> List[ToolMemory]:\n        \"\"\"Get recently added memories.\"\"\"\n\n        def _get_recent():\n            client = self._get_client()\n            collection = client.collections.get(self.collection_name)\n\n            # Query and sort by timestamp\n            response = collection.query.fetch_objects(limit=1000)\n\n            # Convert to list and sort\n            objects_list = list(response.objects)\n            sorted_objects = sorted(\n                objects_list,\n                key=lambda o: o.properties.get(\"timestamp\", \"\"),\n                reverse=True,\n            )\n\n            memories = []\n            for obj in sorted_objects[:limit]:\n                properties = obj.properties\n                args = json.loads(properties.get(\"args_json\", \"{}\"))\n                metadata_dict = json.loads(properties.get(\"metadata_json\", \"{}\"))\n\n                memory = ToolMemory(\n                    memory_id=str(obj.uuid),\n                    question=properties.get(\"question\"),\n                    tool_name=properties.get(\"tool_name\"),\n                    args=args,\n                    timestamp=properties.get(\"timestamp\"),\n                    success=properties.get(\"success\", True),\n                    metadata=metadata_dict,\n                )\n                memories.append(memory)\n\n            return memories\n\n        return await asyncio.get_event_loop().run_in_executor(\n            self._executor, _get_recent\n        )\n\n    async def delete_by_id(self, context: ToolContext, memory_id: str) -> bool:\n        \"\"\"Delete a memory by its ID.\"\"\"\n\n        def _delete():\n            client = self._get_client()\n            collection = client.collections.get(self.collection_name)\n\n            try:\n                collection.data.delete_by_id(uuid=memory_id)\n                return True\n            except Exception:\n                return False\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _delete)\n\n    async def save_text_memory(self, content: str, context: ToolContext) -> TextMemory:\n        \"\"\"Save a text memory.\"\"\"\n\n        def _save():\n            client = self._get_client()\n            collection = client.collections.get(self.collection_name)\n\n            memory_id = str(uuid.uuid4())\n            timestamp = datetime.now().isoformat()\n            embedding = self._create_embedding(content)\n\n            properties = {\n                \"question\": content,  # Using question field for content\n                \"tool_name\": \"\",  # Empty for text memories\n                \"args_json\": \"\",\n                \"timestamp\": timestamp,\n                \"success\": True,\n                \"metadata_json\": json.dumps({\"is_text_memory\": True}),\n            }\n\n            collection.data.insert(\n                properties=properties, vector=embedding, uuid=memory_id\n            )\n\n            return TextMemory(memory_id=memory_id, content=content, timestamp=timestamp)\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _save)\n\n    async def search_text_memories(\n        self,\n        query: str,\n        context: ToolContext,\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n    ) -> List[TextMemorySearchResult]:\n        \"\"\"Search for similar text memories.\"\"\"\n\n        def _search():\n            client = self._get_client()\n            collection = client.collections.get(self.collection_name)\n\n            embedding = self._create_embedding(query)\n\n            # Build filter for text memories (empty tool_name)\n            filters = weaviate.classes.query.Filter.by_property(\"tool_name\").equal(\"\")\n\n            response = collection.query.near_vector(\n                near_vector=embedding,\n                limit=limit,\n                filters=filters,\n                return_metadata=weaviate.classes.query.MetadataQuery(distance=True),\n            )\n\n            search_results = []\n            for i, obj in enumerate(response.objects):\n                distance = obj.metadata.distance if obj.metadata else 1.0\n                similarity_score = 1 - distance\n\n                if similarity_score >= similarity_threshold:\n                    properties = obj.properties\n                    content = properties.get(\"question\", \"\")\n\n                    memory = TextMemory(\n                        memory_id=str(obj.uuid),\n                        content=content,\n                        timestamp=properties.get(\"timestamp\"),\n                    )\n\n                    search_results.append(\n                        TextMemorySearchResult(\n                            memory=memory, similarity_score=similarity_score, rank=i + 1\n                        )\n                    )\n\n            return search_results\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _search)\n\n    async def get_recent_text_memories(\n        self, context: ToolContext, limit: int = 10\n    ) -> List[TextMemory]:\n        \"\"\"Get recently added text memories.\"\"\"\n\n        def _get_recent():\n            client = self._get_client()\n            collection = client.collections.get(self.collection_name)\n\n            # Query text memories (empty tool_name) and sort by timestamp\n            response = collection.query.fetch_objects(\n                filters=weaviate.classes.query.Filter.by_property(\"tool_name\").equal(\n                    \"\"\n                ),\n                limit=1000,\n            )\n\n            # Convert to list and sort\n            objects_list = list(response.objects)\n            sorted_objects = sorted(\n                objects_list,\n                key=lambda o: o.properties.get(\"timestamp\", \"\"),\n                reverse=True,\n            )\n\n            memories = []\n            for obj in sorted_objects[:limit]:\n                properties = obj.properties\n                content = properties.get(\"question\", \"\")\n\n                memory = TextMemory(\n                    memory_id=str(obj.uuid),\n                    content=content,\n                    timestamp=properties.get(\"timestamp\"),\n                )\n                memories.append(memory)\n\n            return memories\n\n        return await asyncio.get_event_loop().run_in_executor(\n            self._executor, _get_recent\n        )\n\n    async def delete_text_memory(self, context: ToolContext, memory_id: str) -> bool:\n        \"\"\"Delete a text memory by its ID.\"\"\"\n\n        def _delete():\n            client = self._get_client()\n            collection = client.collections.get(self.collection_name)\n\n            try:\n                collection.data.delete_by_id(uuid=memory_id)\n                return True\n            except Exception:\n                return False\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _delete)\n\n    async def clear_memories(\n        self,\n        context: ToolContext,\n        tool_name: Optional[str] = None,\n        before_date: Optional[str] = None,\n    ) -> int:\n        \"\"\"Clear stored memories.\"\"\"\n\n        def _clear():\n            client = self._get_client()\n            collection = client.collections.get(self.collection_name)\n\n            # Build filter\n            if tool_name and before_date:\n                filters = weaviate.classes.query.Filter.by_property(\"tool_name\").equal(\n                    tool_name\n                ) & weaviate.classes.query.Filter.by_property(\"timestamp\").less_than(\n                    before_date\n                )\n            elif tool_name:\n                filters = weaviate.classes.query.Filter.by_property(\"tool_name\").equal(\n                    tool_name\n                )\n            elif before_date:\n                filters = weaviate.classes.query.Filter.by_property(\n                    \"timestamp\"\n                ).less_than(before_date)\n            else:\n                filters = None\n\n            if filters:\n                collection.data.delete_many(where=filters)\n            else:\n                # Delete all\n                collection.data.delete_many(\n                    where=weaviate.classes.query.Filter.by_property(\n                        \"success\"\n                    ).contains_any([True, False])\n                )\n\n            return 0\n\n        return await asyncio.get_event_loop().run_in_executor(self._executor, _clear)\n"
  },
  {
    "path": "src/vanna/legacy/ZhipuAI/ZhipuAI_Chat.py",
    "content": "import re\nfrom typing import List\n\nimport pandas as pd\nfrom zhipuai import ZhipuAI\n\nfrom ..base import VannaBase\n\n\nclass ZhipuAI_Chat(VannaBase):\n    def __init__(self, config=None):\n        VannaBase.__init__(self, config=config)\n        if config is None:\n            return\n        if \"api_key\" not in config:\n            raise Exception(\"Missing api_key in config\")\n        self.api_key = config[\"api_key\"]\n        self.model = config[\"model\"] if \"model\" in config else \"glm-4\"\n        self.api_url = \"https://open.bigmodel.cn/api/paas/v4/chat/completions\"\n\n    # Static methods similar to those in ZhipuAI_Chat for message formatting and utility\n    @staticmethod\n    def system_message(message: str) -> dict:\n        return {\"role\": \"system\", \"content\": message}\n\n    @staticmethod\n    def user_message(message: str) -> dict:\n        return {\"role\": \"user\", \"content\": message}\n\n    @staticmethod\n    def assistant_message(message: str) -> dict:\n        return {\"role\": \"assistant\", \"content\": message}\n\n    @staticmethod\n    def str_to_approx_token_count(string: str) -> int:\n        return len(string) / 4\n\n    @staticmethod\n    def add_ddl_to_prompt(\n        initial_prompt: str, ddl_list: List[str], max_tokens: int = 14000\n    ) -> str:\n        if len(ddl_list) > 0:\n            initial_prompt += \"\\nYou may use the following DDL statements as a reference for what tables might be available. Use responses to past questions also to guide you:\\n\\n\"\n\n            for ddl in ddl_list:\n                if (\n                    ZhipuAI_Chat.str_to_approx_token_count(initial_prompt)\n                    + ZhipuAI_Chat.str_to_approx_token_count(ddl)\n                    < max_tokens\n                ):\n                    initial_prompt += f\"{ddl}\\n\\n\"\n\n        return initial_prompt\n\n    @staticmethod\n    def add_documentation_to_prompt(\n        initial_prompt: str, documentation_List: List[str], max_tokens: int = 14000\n    ) -> str:\n        if len(documentation_List) > 0:\n            initial_prompt += \"\\nYou may use the following documentation as a reference for what tables might be available. Use responses to past questions also to guide you:\\n\\n\"\n\n            for documentation in documentation_List:\n                if (\n                    ZhipuAI_Chat.str_to_approx_token_count(initial_prompt)\n                    + ZhipuAI_Chat.str_to_approx_token_count(documentation)\n                    < max_tokens\n                ):\n                    initial_prompt += f\"{documentation}\\n\\n\"\n\n        return initial_prompt\n\n    @staticmethod\n    def add_sql_to_prompt(\n        initial_prompt: str, sql_List: List[str], max_tokens: int = 14000\n    ) -> str:\n        if len(sql_List) > 0:\n            initial_prompt += \"\\nYou may use the following SQL statements as a reference for what tables might be available. Use responses to past questions also to guide you:\\n\\n\"\n\n            for question in sql_List:\n                if (\n                    ZhipuAI_Chat.str_to_approx_token_count(initial_prompt)\n                    + ZhipuAI_Chat.str_to_approx_token_count(question[\"sql\"])\n                    < max_tokens\n                ):\n                    initial_prompt += f\"{question['question']}\\n{question['sql']}\\n\\n\"\n\n        return initial_prompt\n\n    def get_sql_prompt(\n        self,\n        question: str,\n        question_sql_list: List,\n        ddl_list: List,\n        doc_list: List,\n        **kwargs,\n    ):\n        initial_prompt = \"The user provides a question and you provide SQL. You will only respond with SQL code and not with any explanations.\\n\\nRespond with only SQL code. Do not answer with any explanations -- just the code.\\n\"\n\n        initial_prompt = ZhipuAI_Chat.add_ddl_to_prompt(\n            initial_prompt, ddl_list, max_tokens=14000\n        )\n\n        initial_prompt = ZhipuAI_Chat.add_documentation_to_prompt(\n            initial_prompt, doc_list, max_tokens=14000\n        )\n\n        message_log = [ZhipuAI_Chat.system_message(initial_prompt)]\n\n        for example in question_sql_list:\n            if example is None:\n                print(\"example is None\")\n            else:\n                if example is not None and \"question\" in example and \"sql\" in example:\n                    message_log.append(ZhipuAI_Chat.user_message(example[\"question\"]))\n                    message_log.append(ZhipuAI_Chat.assistant_message(example[\"sql\"]))\n\n        message_log.append({\"role\": \"user\", \"content\": question})\n\n        return message_log\n\n    def get_followup_questions_prompt(\n        self,\n        question: str,\n        df: pd.DataFrame,\n        question_sql_list: List,\n        ddl_list: List,\n        doc_list: List,\n        **kwargs,\n    ):\n        initial_prompt = f\"The user initially asked the question: '{question}': \\n\\n\"\n\n        initial_prompt = ZhipuAI_Chat.add_ddl_to_prompt(\n            initial_prompt, ddl_list, max_tokens=14000\n        )\n\n        initial_prompt = ZhipuAI_Chat.add_documentation_to_prompt(\n            initial_prompt, doc_list, max_tokens=14000\n        )\n\n        initial_prompt = ZhipuAI_Chat.add_sql_to_prompt(\n            initial_prompt, question_sql_list, max_tokens=14000\n        )\n\n        message_log = [ZhipuAI_Chat.system_message(initial_prompt)]\n        message_log.append(\n            ZhipuAI_Chat.user_message(\n                \"Generate a List of followup questions that the user might ask about this data. Respond with a List of questions, one per line. Do not answer with any explanations -- just the questions.\"\n            )\n        )\n\n        return message_log\n\n    def generate_question(self, sql: str, **kwargs) -> str:\n        response = self.submit_prompt(\n            [\n                self.system_message(\n                    \"The user will give you SQL and you will try to guess what the business question this query is answering. Return just the question without any additional explanation. Do not reference the table name in the question.\"\n                ),\n                self.user_message(sql),\n            ],\n            **kwargs,\n        )\n\n        return response\n\n    def _extract_python_code(self, markdown_string: str) -> str:\n        # Regex pattern to match Python code blocks\n        pattern = r\"```[\\w\\s]*python\\n([\\s\\S]*?)```|```([\\s\\S]*?)```\"\n\n        # Find all matches in the markdown string\n        matches = re.findall(pattern, markdown_string, re.IGNORECASE)\n\n        # Extract the Python code from the matches\n        python_code = []\n        for match in matches:\n            python = match[0] if match[0] else match[1]\n            python_code.append(python.strip())\n\n        if len(python_code) == 0:\n            return markdown_string\n\n        return python_code[0]\n\n    def _sanitize_plotly_code(self, raw_plotly_code: str) -> str:\n        # Remove the fig.show() statement from the plotly code\n        plotly_code = raw_plotly_code.replace(\"fig.show()\", \"\")\n\n        return plotly_code\n\n    def generate_plotly_code(\n        self, question: str = None, sql: str = None, df_metadata: str = None, **kwargs\n    ) -> str:\n        if question is not None:\n            system_msg = f\"The following is a pandas DataFrame that contains the results of the query that answers the question the user asked: '{question}'\"\n        else:\n            system_msg = \"The following is a pandas DataFrame \"\n\n        if sql is not None:\n            system_msg += f\"\\n\\nThe DataFrame was produced using this query: {sql}\\n\\n\"\n\n        system_msg += f\"The following is information about the resulting pandas DataFrame 'df': \\n{df_metadata}\"\n\n        message_log = [\n            self.system_message(system_msg),\n            self.user_message(\n                \"Can you generate the Python plotly code to chart the results of the dataframe? Assume the data is in a pandas dataframe called 'df'. If there is only one value in the dataframe, use an Indicator. Respond with only Python code. Do not answer with any explanations -- just the code.\"\n            ),\n        ]\n\n        plotly_code = self.submit_prompt(message_log, kwargs=kwargs)\n\n        return self._sanitize_plotly_code(self._extract_python_code(plotly_code))\n\n    def submit_prompt(\n        self, prompt, max_tokens=500, temperature=0.7, top_p=0.7, stop=None, **kwargs\n    ):\n        if prompt is None:\n            raise Exception(\"Prompt is None\")\n\n        if len(prompt) == 0:\n            raise Exception(\"Prompt is empty\")\n\n        client = ZhipuAI(api_key=self.api_key)\n        response = client.chat.completions.create(\n            model=\"glm-4\",\n            max_tokens=max_tokens,\n            temperature=temperature,\n            top_p=top_p,\n            stop=stop,\n            messages=prompt,\n        )\n\n        return response.choices[0].message.content\n"
  },
  {
    "path": "src/vanna/legacy/ZhipuAI/ZhipuAI_embeddings.py",
    "content": "from typing import List\nfrom zhipuai import ZhipuAI\nfrom chromadb import Documents, EmbeddingFunction, Embeddings\nfrom ..base import VannaBase\n\n\nclass ZhipuAI_Embeddings(VannaBase):\n    \"\"\"\n    [future functionality] This function is used to generate embeddings from ZhipuAI.\n\n    Args:\n        VannaBase (_type_): _description_\n    \"\"\"\n\n    def __init__(self, config=None):\n        VannaBase.__init__(self, config=config)\n        if \"api_key\" not in config:\n            raise Exception(\"Missing api_key in config\")\n        self.api_key = config[\"api_key\"]\n        self.client = ZhipuAI(api_key=self.api_key)\n\n    def generate_embedding(self, data: str, **kwargs) -> List[float]:\n        embedding = self.client.embeddings.create(\n            model=\"embedding-2\",\n            input=data,\n        )\n\n        return embedding.data[0].embedding\n\n\nclass ZhipuAIEmbeddingFunction(EmbeddingFunction[Documents]):\n    \"\"\"\n    A embeddingFunction that uses ZhipuAI to generate embeddings which can use in chromadb.\n    usage:\n    class MyVanna(ChromaDB_VectorStore, ZhipuAI_Chat):\n        def __init__(self, config=None):\n            ChromaDB_VectorStore.__init__(self, config=config)\n            ZhipuAI_Chat.__init__(self, config=config)\n\n    config={'api_key': 'xxx'}\n    zhipu_embedding_function = ZhipuAIEmbeddingFunction(config=config)\n    config = {\"api_key\": \"xxx\", \"model\": \"glm-4\",\"path\":\"xy\",\"embedding_function\":zhipu_embedding_function}\n\n    vn = MyVanna(config)\n\n    \"\"\"\n\n    def __init__(self, config=None):\n        if config is None or \"api_key\" not in config:\n            raise ValueError(\"Missing 'api_key' in config\")\n\n        self.api_key = config[\"api_key\"]\n        self.model_name = config.get(\"model_name\", \"embedding-2\")\n\n        try:\n            self.client = ZhipuAI(api_key=self.api_key)\n        except Exception as e:\n            raise ValueError(f\"Error initializing ZhipuAI client: {e}\")\n\n    def __call__(self, input: Documents) -> Embeddings:\n        # Replace newlines, which can negatively affect performance.\n        input = [t.replace(\"\\n\", \" \") for t in input]\n        all_embeddings = []\n        print(f\"Generating embeddings for {len(input)} documents\")\n\n        # Iterating over each document for individual API calls\n        for document in input:\n            try:\n                response = self.client.embeddings.create(\n                    model=self.model_name, input=document\n                )\n                # print(response)\n                embedding = response.data[0].embedding\n                all_embeddings.append(embedding)\n                # print(f\"Cost required: {response.usage.total_tokens}\")\n            except Exception as e:\n                raise ValueError(f\"Error generating embedding for document: {e}\")\n\n        return all_embeddings\n"
  },
  {
    "path": "src/vanna/legacy/ZhipuAI/__init__.py",
    "content": "from .ZhipuAI_Chat import ZhipuAI_Chat\nfrom .ZhipuAI_embeddings import ZhipuAI_Embeddings, ZhipuAIEmbeddingFunction\n"
  },
  {
    "path": "src/vanna/legacy/__init__.py",
    "content": "import dataclasses\nimport json\nimport os\nfrom dataclasses import dataclass\nfrom typing import Callable, List, Tuple, Union\n\nimport pandas as pd\nimport requests\nimport plotly.graph_objs\n\nfrom .exceptions import (\n    OTPCodeError,\n    ValidationError,\n)\nfrom .types import (\n    ApiKey,\n    Status,\n    TrainingData,\n    UserEmail,\n    UserOTP,\n)\nfrom .utils import sanitize_model_name, validate_config_path\n\napi_key: Union[str, None] = None  # API key for Vanna.AI\n\nfig_as_img: bool = False  # Whether or not to return Plotly figures as images\n\nrun_sql: Union[Callable[[str], pd.DataFrame], None] = (\n    None  # Function to convert SQL to a Pandas DataFrame\n)\n\"\"\"\n**Example**\n```python\nvn.run_sql = lambda sql: pd.read_sql(sql, engine)\n```\n\nSet the SQL to DataFrame function for Vanna.AI. This is used in the [`vn.ask(...)`][vanna.ask] function.\nInstead of setting this directly you can also use [`vn.connect_to_snowflake(...)`][vanna.connect_to_snowflake] to set this.\n\n\"\"\"\n\n__org: Union[str, None] = None  # Organization name for Vanna.AI\n\n_unauthenticated_endpoint = \"https://ask.vanna.ai/unauthenticated_rpc\"\n\n\ndef error_deprecation():\n    raise Exception(\"\"\"\nPlease switch to the following method for initializing Vanna:\n\nfrom vanna.remote import VannaDefault\n\napi_key = # Your API key from https://vanna.ai/account/profile \nvanna_model_name = # Your model name from https://vanna.ai/account/profile\n                    \nvn = VannaDefault(model=vanna_model_name, api_key=api_key)\n\"\"\")\n\n\ndef __unauthenticated_rpc_call(method, params):\n    headers = {\n        \"Content-Type\": \"application/json\",\n    }\n    data = {\"method\": method, \"params\": [__dataclass_to_dict(obj) for obj in params]}\n\n    response = requests.post(\n        _unauthenticated_endpoint, headers=headers, data=json.dumps(data)\n    )\n    return response.json()\n\n\ndef __dataclass_to_dict(obj):\n    return dataclasses.asdict(obj)\n\n\ndef get_api_key(email: str, otp_code: Union[str, None] = None) -> str:\n    \"\"\"\n    **Example:**\n    ```python\n    vn.get_api_key(email=\"my-email@example.com\")\n    ```\n\n    Login to the Vanna.AI API.\n\n    Args:\n        email (str): The email address to login with.\n        otp_code (Union[str, None]): The OTP code to login with. If None, an OTP code will be sent to the email address.\n\n    Returns:\n        str: The API key.\n    \"\"\"\n    vanna_api_key = os.environ.get(\"VANNA_API_KEY\", None)\n\n    if vanna_api_key is not None:\n        return vanna_api_key\n\n    if email == \"my-email@example.com\":\n        raise ValidationError(\n            \"Please replace 'my-email@example.com' with your email address.\"\n        )\n\n    if otp_code is None:\n        params = [UserEmail(email=email)]\n\n        d = __unauthenticated_rpc_call(method=\"send_otp\", params=params)\n\n        if \"result\" not in d:\n            raise OTPCodeError(\"Error sending OTP code.\")\n\n        status = Status(**d[\"result\"])\n\n        if not status.success:\n            raise OTPCodeError(f\"Error sending OTP code: {status.message}\")\n\n        otp_code = input(\"Check your email for the code and enter it here: \")\n\n    params = [UserOTP(email=email, otp=otp_code)]\n\n    d = __unauthenticated_rpc_call(method=\"verify_otp\", params=params)\n\n    if \"result\" not in d:\n        raise OTPCodeError(\"Error verifying OTP code.\")\n\n    key = ApiKey(**d[\"result\"])\n\n    if key is None:\n        raise OTPCodeError(\"Error verifying OTP code.\")\n\n    api_key = key.key\n\n    return api_key\n\n\ndef set_api_key(key: str) -> None:\n    error_deprecation()\n\n\ndef get_models() -> List[str]:\n    error_deprecation()\n\n\ndef create_model(model: str, db_type: str) -> bool:\n    error_deprecation()\n\n\ndef add_user_to_model(model: str, email: str, is_admin: bool) -> bool:\n    error_deprecation()\n\n\ndef update_model_visibility(public: bool) -> bool:\n    error_deprecation()\n\n\ndef set_model(model: str):\n    error_deprecation()\n\n\ndef add_sql(\n    question: str, sql: str, tag: Union[str, None] = \"Manually Trained\"\n) -> bool:\n    error_deprecation()\n\n\ndef add_ddl(ddl: str) -> bool:\n    error_deprecation()\n\n\ndef add_documentation(documentation: str) -> bool:\n    error_deprecation()\n\n\n@dataclass\nclass TrainingPlanItem:\n    item_type: str\n    item_group: str\n    item_name: str\n    item_value: str\n\n    def __str__(self):\n        if self.item_type == self.ITEM_TYPE_SQL:\n            return f\"Train on SQL: {self.item_group} {self.item_name}\"\n        elif self.item_type == self.ITEM_TYPE_DDL:\n            return f\"Train on DDL: {self.item_group} {self.item_name}\"\n        elif self.item_type == self.ITEM_TYPE_IS:\n            return f\"Train on Information Schema: {self.item_group} {self.item_name}\"\n\n    ITEM_TYPE_SQL = \"sql\"\n    ITEM_TYPE_DDL = \"ddl\"\n    ITEM_TYPE_IS = \"is\"\n\n\nclass TrainingPlan:\n    \"\"\"\n    A class representing a training plan. You can see what's in it, and remove items from it that you don't want trained.\n\n    **Example:**\n    ```python\n    plan = vn.get_training_plan()\n\n    plan.get_summary()\n    ```\n\n    \"\"\"\n\n    _plan: List[TrainingPlanItem]\n\n    def __init__(self, plan: List[TrainingPlanItem]):\n        self._plan = plan\n\n    def __str__(self):\n        return \"\\n\".join(self.get_summary())\n\n    def __repr__(self):\n        return self.__str__()\n\n    def get_summary(self) -> List[str]:\n        \"\"\"\n        **Example:**\n        ```python\n        plan = vn.get_training_plan()\n\n        plan.get_summary()\n        ```\n\n        Get a summary of the training plan.\n\n        Returns:\n            List[str]: A list of strings describing the training plan.\n        \"\"\"\n\n        return [f\"{item}\" for item in self._plan]\n\n    def remove_item(self, item: str):\n        \"\"\"\n        **Example:**\n        ```python\n        plan = vn.get_training_plan()\n\n        plan.remove_item(\"Train on SQL: What is the average salary of employees?\")\n        ```\n\n        Remove an item from the training plan.\n\n        Args:\n            item (str): The item to remove.\n        \"\"\"\n        for plan_item in self._plan:\n            if str(plan_item) == item:\n                self._plan.remove(plan_item)\n                break\n\n\ndef get_training_plan_postgres(\n    filter_databases: Union[List[str], None] = None,\n    filter_schemas: Union[List[str], None] = None,\n    include_information_schema: bool = False,\n    use_historical_queries: bool = True,\n) -> TrainingPlan:\n    error_deprecation()\n\n\ndef get_training_plan_generic(df) -> TrainingPlan:\n    error_deprecation()\n\n\ndef get_training_plan_experimental(\n    filter_databases: Union[List[str], None] = None,\n    filter_schemas: Union[List[str], None] = None,\n    include_information_schema: bool = False,\n    use_historical_queries: bool = True,\n) -> TrainingPlan:\n    error_deprecation()\n\n\ndef train(\n    question: str = None,\n    sql: str = None,\n    ddl: str = None,\n    documentation: str = None,\n    json_file: str = None,\n    sql_file: str = None,\n    plan: TrainingPlan = None,\n) -> bool:\n    error_deprecation()\n\n\ndef flag_sql_for_review(\n    question: str, sql: Union[str, None] = None, error_msg: Union[str, None] = None\n) -> bool:\n    error_deprecation()\n\n\ndef remove_sql(question: str) -> bool:\n    error_deprecation()\n\n\ndef remove_training_data(id: str) -> bool:\n    error_deprecation()\n\n\ndef generate_sql(question: str) -> str:\n    error_deprecation()\n\n\ndef get_related_training_data(question: str) -> TrainingData:\n    error_deprecation()\n\n\ndef generate_meta(question: str) -> str:\n    error_deprecation()\n\n\ndef generate_followup_questions(question: str, df: pd.DataFrame) -> List[str]:\n    error_deprecation()\n\n\ndef generate_questions() -> List[str]:\n    error_deprecation()\n\n\ndef ask(\n    question: Union[str, None] = None,\n    print_results: bool = True,\n    auto_train: bool = True,\n    generate_followups: bool = True,\n) -> Union[\n    Tuple[\n        Union[str, None],\n        Union[pd.DataFrame, None],\n        Union[plotly.graph_objs.Figure, None],\n        Union[List[str], None],\n    ],\n    None,\n]:\n    error_deprecation()\n\n\ndef generate_plotly_code(\n    question: Union[str, None],\n    sql: Union[str, None],\n    df: pd.DataFrame,\n    chart_instructions: Union[str, None] = None,\n) -> str:\n    error_deprecation()\n\n\ndef get_plotly_figure(\n    plotly_code: str, df: pd.DataFrame, dark_mode: bool = True\n) -> plotly.graph_objs.Figure:\n    error_deprecation()\n\n\ndef get_results(cs, default_database: str, sql: str) -> pd.DataFrame:\n    error_deprecation()\n\n\ndef generate_explanation(sql: str) -> str:\n    error_deprecation()\n\n\ndef generate_question(sql: str) -> str:\n    error_deprecation()\n\n\ndef get_all_questions() -> pd.DataFrame:\n    error_deprecation()\n\n\ndef get_training_data() -> pd.DataFrame:\n    error_deprecation()\n\n\ndef connect_to_sqlite(url: str):\n    error_deprecation()\n\n\ndef connect_to_snowflake(\n    account: str,\n    username: str,\n    password: str,\n    database: str,\n    schema: Union[str, None] = None,\n    role: Union[str, None] = None,\n):\n    error_deprecation()\n\n\ndef connect_to_postgres(\n    host: str = None,\n    dbname: str = None,\n    user: str = None,\n    password: str = None,\n    port: int = None,\n):\n    error_deprecation()\n\n\ndef connect_to_bigquery(cred_file_path: str = None, project_id: str = None):\n    error_deprecation()\n\n\ndef connect_to_duckdb(url: str = \"memory\", init_sql: str = None):\n    error_deprecation()\n"
  },
  {
    "path": "src/vanna/legacy/adapter.py",
    "content": "\"\"\"\nLegacy VannaBase adapter for the Vanna Agents framework.\n\nThis module provides a LegacyVannaAdapter that bridges legacy VannaBase objects\nwith the new ToolRegistry system by auto-registering legacy methods as tools\nwith appropriate group-based access control.\n\"\"\"\n\nfrom typing import Any, Dict, List, Optional\n\nimport pandas as pd\n\nfrom ..capabilities.agent_memory import (\n    AgentMemory,\n    TextMemory,\n    TextMemorySearchResult,\n    ToolMemory,\n    ToolMemorySearchResult,\n)\nfrom ..capabilities.sql_runner import SqlRunner, RunSqlToolArgs\nfrom ..core.registry import ToolRegistry\nfrom ..core.tool import Tool, ToolContext, ToolResult\nfrom ..core.user import User\nfrom ..tools.agent_memory import (\n    SaveQuestionToolArgsTool,\n    SearchSavedCorrectToolUsesTool,\n)\nfrom ..tools.run_sql import RunSqlTool\nfrom .base.base import VannaBase\n\n\nclass LegacySqlRunner(SqlRunner):\n    \"\"\"SqlRunner implementation that wraps a legacy VannaBase instance.\n\n    This class bridges the new SqlRunner interface with legacy VannaBase\n    run_sql methods, allowing legacy database connections to work with\n    the new tool-based architecture.\n    \"\"\"\n\n    def __init__(self, vn: VannaBase):\n        \"\"\"Initialize with a legacy VannaBase instance.\n\n        Args:\n            vn: The legacy VannaBase instance with an initialized run_sql method\n        \"\"\"\n        self.vn = vn\n\n    async def run_sql(self, args: RunSqlToolArgs, context: ToolContext) -> pd.DataFrame:\n        \"\"\"Execute SQL query using the legacy VannaBase run_sql method.\n\n        Args:\n            args: SQL query arguments containing the SQL string\n            context: Tool execution context (not used by legacy implementation)\n\n        Returns:\n            DataFrame with query results\n\n        Raises:\n            Exception: If query execution fails\n        \"\"\"\n        # Call the legacy VannaBase run_sql method\n        # The legacy method is synchronous, so we call it directly\n        return self.vn.run_sql(args.sql)\n\n\nclass LegacyVannaAdapter(ToolRegistry, AgentMemory):\n    \"\"\"Adapter that wraps a legacy VannaBase object and exposes its methods as tools.\n\n    This adapter automatically registers specific VannaBase methods as tools in the\n    registry with configurable group-based access control. This allows legacy Vanna\n    instances to work seamlessly with the new Agents framework.\n\n    Features:\n    - Auto-registers legacy methods as tools\n    - Configurable group-based permissions ('user', 'admin', etc.)\n    - Seamless integration with ToolRegistry\n    - Implements AgentMemory interface\n    - Preserves legacy VannaBase functionality\n\n    Example:\n        ```python\n        from vanna.legacy.base import VannaBase\n        from vanna.legacy.adapter import LegacyVannaAdapter\n\n        # Initialize your legacy Vanna instance\n        vn = VannaBase(config={\"model\": \"gpt-4\"})\n        vn.connect_to_postgres(...)\n\n        # Create adapter and auto-register tools\n        adapter = LegacyVannaAdapter(vn)\n\n        # Tools are now available through the registry\n        schemas = await adapter.get_schemas(user)\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        vn: VannaBase,\n        audit_logger: Optional[Any] = None,\n        audit_config: Optional[Any] = None,\n    ) -> None:\n        \"\"\"Initialize the adapter with a legacy VannaBase instance.\n\n        Args:\n            vanna: The legacy VannaBase instance to wrap\n            audit_logger: Optional audit logger for tool execution tracking\n            audit_config: Optional audit configuration\n        \"\"\"\n        ToolRegistry.__init__(\n            self, audit_logger=audit_logger, audit_config=audit_config\n        )\n        self.vn = vn\n        self._register_tools()\n\n    def _register_tools(self) -> None:\n        \"\"\"Register legacy VannaBase methods as tools with appropriate permissions.\n\n        Registers the following tools:\n        - RunSqlTool: Wraps the legacy run_sql method via LegacySqlRunner\n        - SaveQuestionToolArgsTool: Wraps add_question_sql via LegacyAgentMemory\n        - SearchSavedCorrectToolUsesTool: Wraps get_similar_question_sql via LegacyAgentMemory\n        \"\"\"\n        # Create a LegacySqlRunner to wrap the VannaBase run_sql method\n        sql_runner = LegacySqlRunner(self.vn)\n\n        # Register the RunSqlTool with user and admin access\n        run_sql_tool = RunSqlTool(sql_runner)\n        self.register_local_tool(run_sql_tool, access_groups=[\"user\", \"admin\"])\n\n        # Register memory tools using the internal _agent_memory instance\n        # SaveQuestionToolArgsTool - for saving question-tool-args patterns (admin only)\n        save_memory_tool = SaveQuestionToolArgsTool()\n        self.register_local_tool(save_memory_tool, access_groups=[\"admin\"])\n\n        # SearchSavedCorrectToolUsesTool - for searching similar patterns (user and admin)\n        search_memory_tool = SearchSavedCorrectToolUsesTool()\n        self.register_local_tool(search_memory_tool, access_groups=[\"user\", \"admin\"])\n\n    # AgentMemory interface implementation\n\n    async def save_tool_usage(\n        self,\n        question: str,\n        tool_name: str,\n        args: Dict[str, Any],\n        context: ToolContext,\n        success: bool = True,\n        metadata: Optional[Dict[str, Any]] = None,\n    ) -> None:\n        \"\"\"Save a tool usage pattern by storing it as a question-sql pair.\n\n        Args:\n            question: The user question\n            tool_name: Name of the tool that was used\n            args: Arguments passed to the tool\n            context: Tool execution context (not used by legacy implementation)\n            success: Whether the tool execution was successful\n            metadata: Additional metadata (not used by legacy implementation)\n        \"\"\"\n        # For legacy compatibility, we primarily care about SQL queries\n        # Extract SQL from args if this was a run_sql tool\n        if tool_name == \"run_sql\" and \"sql\" in args:\n            sql = args[\"sql\"]\n            # Call the legacy add_question_sql method\n            # The legacy method is synchronous, so we call it directly\n            self.vn.add_question_sql(question=question, sql=sql)\n\n    async def search_similar_usage(\n        self,\n        question: str,\n        context: ToolContext,\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n        tool_name_filter: Optional[str] = None,\n    ) -> List[ToolMemorySearchResult]:\n        \"\"\"Search for similar tool usage patterns using legacy question-sql lookup.\n\n        Args:\n            question: The question to search for\n            context: Tool execution context (not used by legacy implementation)\n            limit: Maximum number of results (not directly supported by legacy)\n            similarity_threshold: Minimum similarity score (not directly supported by legacy)\n            tool_name_filter: Filter by tool name (not directly supported by legacy)\n\n        Returns:\n            List of memory search results with similar question-sql pairs\n        \"\"\"\n        # Call the legacy get_similar_question_sql method\n        similar_results = self.vn.get_similar_question_sql(question=question)\n\n        # Convert legacy results to ToolMemorySearchResult format\n        memory_results = []\n        for idx, result in enumerate(similar_results):\n            # Legacy results are typically dicts with 'question' and 'sql' keys\n            if isinstance(result, dict) and \"question\" in result and \"sql\" in result:\n                tool_memory = ToolMemory(\n                    memory_id=None,  # Legacy doesn't provide IDs\n                    question=result[\"question\"],\n                    tool_name=\"run_sql\",\n                    args={\"sql\": result[\"sql\"]},\n                    success=True,\n                )\n\n                # Assign a simple rank-based similarity score\n                # Legacy system doesn't provide actual similarity scores\n                similarity_score = 1.0 - (idx * 0.1)  # Decreasing score by rank\n                similarity_score = max(similarity_score, 0.0)\n\n                memory_results.append(\n                    ToolMemorySearchResult(\n                        memory=tool_memory,\n                        similarity_score=similarity_score,\n                        rank=idx + 1,\n                    )\n                )\n\n        return memory_results[:limit]\n\n    async def save_text_memory(self, content: str, context: ToolContext) -> TextMemory:\n        \"\"\"Save text memory using legacy add_documentation method.\n\n        Args:\n            content: The documentation content to save\n            context: Tool execution context (not used by legacy implementation)\n\n        Returns:\n            TextMemory object with the saved content\n        \"\"\"\n        # Call the legacy add_documentation method\n        # The legacy method is synchronous, so we call it directly\n        doc_id = self.vn.add_documentation(documentation=content)\n\n        return TextMemory(\n            memory_id=doc_id,\n            content=content,\n            timestamp=None,  # Legacy doesn't provide timestamps\n        )\n\n    async def search_text_memories(\n        self,\n        query: str,\n        context: ToolContext,\n        *,\n        limit: int = 10,\n        similarity_threshold: float = 0.7,\n    ) -> List[TextMemorySearchResult]:\n        \"\"\"Search text memories using legacy get_related_documentation method.\n\n        Args:\n            query: The query to search for\n            context: Tool execution context (not used by legacy implementation)\n            limit: Maximum number of results (not directly supported by legacy)\n            similarity_threshold: Minimum similarity score (not directly supported by legacy)\n\n        Returns:\n            List of text memory search results\n        \"\"\"\n        # Call the legacy get_related_documentation method\n        related_docs = self.vn.get_related_documentation(question=query)\n\n        # Convert legacy results to TextMemorySearchResult format\n        memory_results = []\n        for idx, doc in enumerate(related_docs):\n            # Legacy results are typically strings or dicts\n            if isinstance(doc, str):\n                content = doc\n                doc_id = None\n            elif isinstance(doc, dict):\n                content = str(doc.get(\"documentation\", doc.get(\"content\", str(doc))))\n                doc_id = doc.get(\"id\")\n            else:\n                content = str(doc)\n                doc_id = None\n\n            # Create TextMemory object\n            text_memory = TextMemory(\n                memory_id=doc_id,\n                content=content,\n                timestamp=None,  # Legacy doesn't provide timestamps\n            )\n\n            # Assign a simple rank-based similarity score\n            # Legacy system doesn't provide actual similarity scores\n            similarity_score = 1.0 - (idx * 0.1)  # Decreasing score by rank\n            similarity_score = max(similarity_score, 0.0)\n\n            if similarity_score >= similarity_threshold:\n                memory_results.append(\n                    TextMemorySearchResult(\n                        memory=text_memory,\n                        similarity_score=similarity_score,\n                        rank=idx + 1,\n                    )\n                )\n\n        return memory_results[:limit]\n\n    async def get_recent_memories(\n        self, context: ToolContext, limit: int = 10\n    ) -> List[ToolMemory]:\n        \"\"\"Get recently added memories.\n\n        Note: Legacy VannaBase does not provide a direct way to get recent memories,\n        so we retrieve using a blank string which typically returns the most relevant\n        or recent items from the vector store.\n\n        Args:\n            context: Tool execution context\n            limit: Maximum number of memories to return\n\n        Returns:\n            List of recently added tool memories\n        \"\"\"\n        # Use blank string retrieval to get recent/relevant memories\n        similar_results = self.vn.get_similar_question_sql(question=\"\")\n\n        # Convert legacy results to ToolMemory format\n        memories = []\n        for idx, result in enumerate(similar_results[:limit]):\n            # Legacy results are typically dicts with 'question' and 'sql' keys\n            if isinstance(result, dict) and \"question\" in result and \"sql\" in result:\n                tool_memory = ToolMemory(\n                    memory_id=None,  # Legacy doesn't provide IDs\n                    question=result[\"question\"],\n                    tool_name=\"run_sql\",\n                    args={\"sql\": result[\"sql\"]},\n                    success=True,\n                )\n                memories.append(tool_memory)\n\n        return memories\n\n    async def get_recent_text_memories(\n        self, context: ToolContext, limit: int = 10\n    ) -> List[TextMemory]:\n        \"\"\"Fetch recently stored text memories.\n\n        Note: Legacy VannaBase does not provide a direct way to get recent text memories,\n        so we retrieve using a blank string which typically returns the most relevant\n        or recent items from the vector store.\n\n        Args:\n            context: Tool execution context\n            limit: Maximum number of memories to return\n\n        Returns:\n            List of recently added text memories\n        \"\"\"\n        # Use blank string retrieval to get recent/relevant documentation\n        related_docs = self.vn.get_related_documentation(question=\"\")\n\n        # Convert legacy results to TextMemory format\n        memories = []\n        for doc in related_docs[:limit]:\n            # Legacy results are typically strings or dicts\n            if isinstance(doc, str):\n                content = doc\n                doc_id = None\n            elif isinstance(doc, dict):\n                content = str(doc.get(\"documentation\", doc.get(\"content\", str(doc))))\n                doc_id = doc.get(\"id\")\n            else:\n                content = str(doc)\n                doc_id = None\n\n            # Create TextMemory object\n            text_memory = TextMemory(\n                memory_id=doc_id,\n                content=content,\n                timestamp=None,  # Legacy doesn't provide timestamps\n            )\n            memories.append(text_memory)\n\n        return memories\n\n    async def delete_by_id(self, context: ToolContext, memory_id: str) -> bool:\n        \"\"\"Delete a memory by its ID using legacy remove_training_data method.\n\n        Args:\n            context: Tool execution context\n            memory_id: ID of the memory to delete\n\n        Returns:\n            True if the memory was deleted, False otherwise\n        \"\"\"\n        # Call the legacy remove_training_data method\n        # The legacy method is synchronous, so we call it directly\n        return self.vn.remove_training_data(id=memory_id)\n\n    async def delete_text_memory(self, context: ToolContext, memory_id: str) -> bool:\n        \"\"\"Delete a text memory by its ID using legacy remove_training_data method.\n\n        Args:\n            context: Tool execution context\n            memory_id: ID of the text memory to delete\n\n        Returns:\n            True if the text memory was deleted, False otherwise\n        \"\"\"\n        # Call the legacy remove_training_data method\n        # The legacy method is synchronous, so we call it directly\n        return self.vn.remove_training_data(id=memory_id)\n\n    async def clear_memories(\n        self,\n        context: ToolContext,\n        tool_name: Optional[str] = None,\n        before_date: Optional[str] = None,\n    ) -> int:\n        \"\"\"Clear stored memories.\n\n        Note: Legacy VannaBase does not provide a direct clear method,\n        so this operation is not supported.\n\n        Args:\n            context: Tool execution context\n            tool_name: Optional tool name filter\n            before_date: Optional date filter\n\n        Returns:\n            0 (operation not supported by legacy)\n        \"\"\"\n        return 0\n\n    # Example stub for a tool wrapper (to be expanded)\n    # You can copy and customize this pattern for each tool you want to expose\n    \"\"\"\n    class ExampleTool(Tool[ExampleToolArgs]):\n        def __init__(self, vanna: VannaBase):\n            self.vanna = vanna\n\n        @property\n        def name(self) -> str:\n            return \"example_tool\"\n\n        @property\n        def description(self) -> str:\n            return \"Example tool description\"\n\n        @property\n        def access_groups(self) -> List[str]:\n            # This is optional - will be overridden by register_local_tool\n            return []\n\n        def get_args_schema(self) -> type[ExampleToolArgs]:\n            return ExampleToolArgs\n\n        async def execute(\n            self,\n            context: ToolContext,\n            args: ExampleToolArgs\n        ) -> ToolResult:\n            # Call the legacy VannaBase method\n            result = self.vanna.example_method(args.param1, args.param2)\n\n            return ToolResult(\n                success=True,\n                result_for_llm=result,\n                ui_component=None,\n            )\n    \"\"\"\n"
  },
  {
    "path": "src/vanna/legacy/advanced/__init__.py",
    "content": "from abc import ABC, abstractmethod\n\n\nclass VannaAdvanced(ABC):\n    def __init__(self, config=None):\n        self.config = config\n\n    @abstractmethod\n    def get_function(self, question: str, additional_data: dict = {}) -> dict:\n        pass\n\n    @abstractmethod\n    def create_function(\n        self, question: str, sql: str, plotly_code: str, **kwargs\n    ) -> dict:\n        pass\n\n    @abstractmethod\n    def update_function(self, old_function_name: str, updated_function: dict) -> bool:\n        pass\n\n    @abstractmethod\n    def delete_function(self, function_name: str) -> bool:\n        pass\n\n    @abstractmethod\n    def get_all_functions(self) -> list:\n        pass\n"
  },
  {
    "path": "src/vanna/legacy/anthropic/__init__.py",
    "content": "from .anthropic_chat import Anthropic_Chat\n"
  },
  {
    "path": "src/vanna/legacy/anthropic/anthropic_chat.py",
    "content": "import os\n\nimport anthropic\n\nfrom ..base import VannaBase\n\n\nclass Anthropic_Chat(VannaBase):\n    def __init__(self, client=None, config=None):\n        VannaBase.__init__(self, config=config)\n\n        # default parameters - can be overrided using config\n        self.temperature = 0.7\n        self.max_tokens = 500\n\n        if \"temperature\" in config:\n            self.temperature = config[\"temperature\"]\n\n        if \"max_tokens\" in config:\n            self.max_tokens = config[\"max_tokens\"]\n\n        if client is not None:\n            self.client = client\n            return\n\n        if config is None and client is None:\n            self.client = anthropic.Anthropic(api_key=os.getenv(\"ANTHROPIC_API_KEY\"))\n            return\n\n        if \"api_key\" in config:\n            self.client = anthropic.Anthropic(api_key=config[\"api_key\"])\n\n    def system_message(self, message: str) -> any:\n        return {\"role\": \"system\", \"content\": message}\n\n    def user_message(self, message: str) -> any:\n        return {\"role\": \"user\", \"content\": message}\n\n    def assistant_message(self, message: str) -> any:\n        return {\"role\": \"assistant\", \"content\": message}\n\n    def submit_prompt(self, prompt, **kwargs) -> str:\n        if prompt is None:\n            raise Exception(\"Prompt is None\")\n\n        if len(prompt) == 0:\n            raise Exception(\"Prompt is empty\")\n\n        # Count the number of tokens in the message log\n        # Use 4 as an approximation for the number of characters per token\n        num_tokens = 0\n        for message in prompt:\n            num_tokens += len(message[\"content\"]) / 4\n\n        if self.config is not None and \"model\" in self.config:\n            print(\n                f\"Using model {self.config['model']} for {num_tokens} tokens (approx)\"\n            )\n            # claude required system message is a single filed\n            # https://docs.anthropic.com/claude/reference/messages_post\n            system_message = \"\"\n            no_system_prompt = []\n            for prompt_message in prompt:\n                role = prompt_message[\"role\"]\n                if role == \"system\":\n                    system_message = prompt_message[\"content\"]\n                else:\n                    no_system_prompt.append(\n                        {\"role\": role, \"content\": prompt_message[\"content\"]}\n                    )\n\n            response = self.client.messages.create(\n                model=self.config[\"model\"],\n                messages=no_system_prompt,\n                system=system_message,\n                max_tokens=self.max_tokens,\n                temperature=self.temperature,\n            )\n\n        return response.content[0].text\n"
  },
  {
    "path": "src/vanna/legacy/azuresearch/__init__.py",
    "content": "from .azuresearch_vector import AzureAISearch_VectorStore\n"
  },
  {
    "path": "src/vanna/legacy/azuresearch/azuresearch_vector.py",
    "content": "import ast\nimport json\nfrom typing import List\n\nimport pandas as pd\nfrom azure.core.credentials import AzureKeyCredential\nfrom azure.search.documents import SearchClient\nfrom azure.search.documents.indexes import SearchIndexClient\nfrom azure.search.documents.indexes.models import (\n    ExhaustiveKnnAlgorithmConfiguration,\n    ExhaustiveKnnParameters,\n    SearchableField,\n    SearchField,\n    SearchFieldDataType,\n    SearchIndex,\n    VectorSearch,\n    VectorSearchAlgorithmKind,\n    VectorSearchAlgorithmMetric,\n    VectorSearchProfile,\n)\nfrom azure.search.documents.models import VectorFilterMode, VectorizedQuery\nfrom fastembed import TextEmbedding\n\nfrom ..base import VannaBase\nfrom ..utils import deterministic_uuid\n\n\nclass AzureAISearch_VectorStore(VannaBase):\n    \"\"\"\n    AzureAISearch_VectorStore is a class that provides a vector store for Azure AI Search.\n\n    Args:\n        config (dict): Configuration dictionary. Defaults to {}. You must provide an API key in the config.\n            - azure_search_endpoint (str, optional): Azure Search endpoint. Defaults to \"https://azcognetive.search.windows.net\".\n            - azure_search_api_key (str): Azure Search API key.\n            - dimensions (int, optional): Dimensions of the embeddings. Defaults to 384 which corresponds to the dimensions of BAAI/bge-small-en-v1.5.\n            - fastembed_model (str, optional): Fastembed model to use. Defaults to \"BAAI/bge-small-en-v1.5\".\n            - index_name (str, optional): Name of the index. Defaults to \"vanna-index\".\n            - n_results (int, optional): Number of results to return. Defaults to 10.\n            - n_results_ddl (int, optional): Number of results to return for DDL queries. Defaults to the value of n_results.\n            - n_results_sql (int, optional): Number of results to return for SQL queries. Defaults to the value of n_results.\n            - n_results_documentation (int, optional): Number of results to return for documentation queries. Defaults to the value of n_results.\n\n    Raises:\n        ValueError: If config is None, or if 'azure_search_api_key' is not provided in the config.\n    \"\"\"\n\n    def __init__(self, config=None):\n        VannaBase.__init__(self, config=config)\n\n        self.config = config or None\n\n        if config is None:\n            raise ValueError(\n                \"config is required, pass an API key, 'azure_search_api_key', in the config.\"\n            )\n\n        azure_search_endpoint = config.get(\n            \"azure_search_endpoint\", \"https://azcognetive.search.windows.net\"\n        )\n        azure_search_api_key = config.get(\"azure_search_api_key\")\n\n        self.dimensions = config.get(\"dimensions\", 384)\n        self.fastembed_model = config.get(\"fastembed_model\", \"BAAI/bge-small-en-v1.5\")\n\n        self.index_name = config.get(\"index_name\", \"vanna-index\")\n\n        self.n_results_ddl = config.get(\"n_results_ddl\", config.get(\"n_results\", 10))\n        self.n_results_sql = config.get(\"n_results_sql\", config.get(\"n_results\", 10))\n        self.n_results_documentation = config.get(\n            \"n_results_documentation\", config.get(\"n_results\", 10)\n        )\n\n        if not azure_search_api_key:\n            raise ValueError(\n                \"'azure_search_api_key' is required in config to use AzureAISearch_VectorStore\"\n            )\n\n        self.index_client = SearchIndexClient(\n            endpoint=azure_search_endpoint,\n            credential=AzureKeyCredential(azure_search_api_key),\n        )\n\n        self.search_client = SearchClient(\n            endpoint=azure_search_endpoint,\n            index_name=self.index_name,\n            credential=AzureKeyCredential(azure_search_api_key),\n        )\n\n        if self.index_name not in self._get_indexes():\n            self._create_index()\n\n    def _create_index(self) -> bool:\n        fields = [\n            SearchableField(\n                name=\"id\", type=SearchFieldDataType.String, key=True, filterable=True\n            ),\n            SearchableField(\n                name=\"document\",\n                type=SearchFieldDataType.String,\n                searchable=True,\n                filterable=True,\n            ),\n            SearchField(\n                name=\"type\",\n                type=SearchFieldDataType.String,\n                filterable=True,\n                searchable=True,\n            ),\n            SearchField(\n                name=\"document_vector\",\n                type=SearchFieldDataType.Collection(SearchFieldDataType.Single),\n                searchable=True,\n                vector_search_dimensions=self.dimensions,\n                vector_search_profile_name=\"ExhaustiveKnnProfile\",\n            ),\n        ]\n\n        vector_search = VectorSearch(\n            algorithms=[\n                ExhaustiveKnnAlgorithmConfiguration(\n                    name=\"ExhaustiveKnn\",\n                    kind=VectorSearchAlgorithmKind.EXHAUSTIVE_KNN,\n                    parameters=ExhaustiveKnnParameters(\n                        metric=VectorSearchAlgorithmMetric.COSINE\n                    ),\n                )\n            ],\n            profiles=[\n                VectorSearchProfile(\n                    name=\"ExhaustiveKnnProfile\",\n                    algorithm_configuration_name=\"ExhaustiveKnn\",\n                )\n            ],\n        )\n\n        index = SearchIndex(\n            name=self.index_name, fields=fields, vector_search=vector_search\n        )\n        result = self.index_client.create_or_update_index(index)\n        print(f\"{result.name} created\")\n\n    def _get_indexes(self) -> list:\n        return [index for index in self.index_client.list_index_names()]\n\n    def add_ddl(self, ddl: str) -> str:\n        id = deterministic_uuid(ddl) + \"-ddl\"\n        document = {\n            \"id\": id,\n            \"document\": ddl,\n            \"type\": \"ddl\",\n            \"document_vector\": self.generate_embedding(ddl),\n        }\n        self.search_client.upload_documents(documents=[document])\n        return id\n\n    def add_documentation(self, doc: str) -> str:\n        id = deterministic_uuid(doc) + \"-doc\"\n        document = {\n            \"id\": id,\n            \"document\": doc,\n            \"type\": \"doc\",\n            \"document_vector\": self.generate_embedding(doc),\n        }\n        self.search_client.upload_documents(documents=[document])\n        return id\n\n    def add_question_sql(self, question: str, sql: str) -> str:\n        question_sql_json = json.dumps(\n            {\"question\": question, \"sql\": sql}, ensure_ascii=False\n        )\n        id = deterministic_uuid(question_sql_json) + \"-sql\"\n        document = {\n            \"id\": id,\n            \"document\": question_sql_json,\n            \"type\": \"sql\",\n            \"document_vector\": self.generate_embedding(question_sql_json),\n        }\n        self.search_client.upload_documents(documents=[document])\n        return id\n\n    def get_related_ddl(self, text: str) -> List[str]:\n        result = []\n        vector_query = VectorizedQuery(\n            vector=self.generate_embedding(text), fields=\"document_vector\"\n        )\n        df = pd.DataFrame(\n            self.search_client.search(\n                top=self.n_results_ddl,\n                vector_queries=[vector_query],\n                select=[\"id\", \"document\", \"type\"],\n                filter=f\"type eq 'ddl'\",\n            )\n        )\n\n        if len(df):\n            result = df[\"document\"].tolist()\n        return result\n\n    def get_related_documentation(self, text: str) -> List[str]:\n        result = []\n        vector_query = VectorizedQuery(\n            vector=self.generate_embedding(text), fields=\"document_vector\"\n        )\n\n        df = pd.DataFrame(\n            self.search_client.search(\n                top=self.n_results_documentation,\n                vector_queries=[vector_query],\n                select=[\"id\", \"document\", \"type\"],\n                filter=f\"type eq 'doc'\",\n                vector_filter_mode=VectorFilterMode.PRE_FILTER,\n            )\n        )\n\n        if len(df):\n            result = df[\"document\"].tolist()\n        return result\n\n    def get_similar_question_sql(self, question: str) -> List[str]:\n        result = []\n        # Vectorize the text\n        vector_query = VectorizedQuery(\n            vector=self.generate_embedding(question), fields=\"document_vector\"\n        )\n        df = pd.DataFrame(\n            self.search_client.search(\n                top=self.n_results_sql,\n                vector_queries=[vector_query],\n                select=[\"id\", \"document\", \"type\"],\n                filter=f\"type eq 'sql'\",\n            )\n        )\n\n        if len(df):  # Check if there is similar query and the result is not empty\n            result = [ast.literal_eval(element) for element in df[\"document\"].tolist()]\n\n        return result\n\n    def get_training_data(self) -> List[str]:\n        search = self.search_client.search(\n            search_text=\"*\",\n            select=[\"id\", \"document\", \"type\"],\n            filter=f\"(type eq 'sql') or (type eq 'ddl') or (type eq 'doc')\",\n        ).by_page()\n\n        df = pd.DataFrame([item for page in search for item in page])\n\n        if len(df):\n            df.loc[df[\"type\"] == \"sql\", \"question\"] = df.loc[df[\"type\"] == \"sql\"][\n                \"document\"\n            ].apply(lambda x: json.loads(x)[\"question\"])\n            df.loc[df[\"type\"] == \"sql\", \"content\"] = df.loc[df[\"type\"] == \"sql\"][\n                \"document\"\n            ].apply(lambda x: json.loads(x)[\"sql\"])\n            df.loc[df[\"type\"] != \"sql\", \"content\"] = df.loc[df[\"type\"] != \"sql\"][\n                \"document\"\n            ]\n\n            return df[[\"id\", \"question\", \"content\", \"type\"]]\n\n        return pd.DataFrame()\n\n    def remove_training_data(self, id: str) -> bool:\n        result = self.search_client.delete_documents(documents=[{\"id\": id}])\n        return result[0].succeeded\n\n    def remove_index(self):\n        self.index_client.delete_index(self.index_name)\n\n    def generate_embedding(self, data: str, **kwargs) -> List[float]:\n        embedding_model = TextEmbedding(model_name=self.fastembed_model)\n        embedding = next(embedding_model.embed(data))\n        return embedding.tolist()\n"
  },
  {
    "path": "src/vanna/legacy/base/__init__.py",
    "content": "from .base import VannaBase\n"
  },
  {
    "path": "src/vanna/legacy/base/base.py",
    "content": "r\"\"\"\n\n# Nomenclature\n\n| Prefix | Definition | Examples |\n| --- | --- | --- |\n| `vn.get_` | Fetch some data | [`vn.get_related_ddl(...)`][vanna.base.base.VannaBase.get_related_ddl] |\n| `vn.add_` | Adds something to the retrieval layer | [`vn.add_question_sql(...)`][vanna.base.base.VannaBase.add_question_sql] <br> [`vn.add_ddl(...)`][vanna.base.base.VannaBase.add_ddl] |\n| `vn.generate_` | Generates something using AI based on the information in the model | [`vn.generate_sql(...)`][vanna.base.base.VannaBase.generate_sql] <br> [`vn.generate_explanation()`][vanna.base.base.VannaBase.generate_explanation] |\n| `vn.run_` | Runs code (SQL) | [`vn.run_sql`][vanna.base.base.VannaBase.run_sql] |\n| `vn.remove_` | Removes something from the retrieval layer | [`vn.remove_training_data`][vanna.base.base.VannaBase.remove_training_data] |\n| `vn.connect_` | Connects to a database | [`vn.connect_to_snowflake(...)`][vanna.base.base.VannaBase.connect_to_snowflake] |\n| `vn.update_` | Updates something | N/A -- unused |\n| `vn.set_` | Sets something | N/A -- unused  |\n\n# Open-Source and Extending\n\nVanna.AI is open-source and extensible. If you'd like to use Vanna without the servers, see an example [here](https://vanna.ai/docs/postgres-ollama-chromadb/).\n\nThe following is an example of where various functions are implemented in the codebase when using the default \"local\" version of Vanna. `vanna.base.VannaBase` is the base class which provides a `vanna.base.VannaBase.ask` and `vanna.base.VannaBase.train` function. Those rely on abstract methods which are implemented in the subclasses `vanna.openai_chat.OpenAI_Chat` and `vanna.chromadb_vector.ChromaDB_VectorStore`. `vanna.openai_chat.OpenAI_Chat` uses the OpenAI API to generate SQL and Plotly code. `vanna.chromadb_vector.ChromaDB_VectorStore` uses ChromaDB to store training data and generate embeddings.\n\nIf you want to use Vanna with other LLMs or databases, you can create your own subclass of `vanna.base.VannaBase` and implement the abstract methods.\n\n```mermaid\nflowchart\n    subgraph VannaBase\n        ask\n        train\n    end\n\n    subgraph OpenAI_Chat\n        get_sql_prompt\n        submit_prompt\n        generate_question\n        generate_plotly_code\n    end\n\n    subgraph ChromaDB_VectorStore\n        generate_embedding\n        add_question_sql\n        add_ddl\n        add_documentation\n        get_similar_question_sql\n        get_related_ddl\n        get_related_documentation\n    end\n```\n\n\"\"\"\n\nimport json\nimport os\nimport re\nimport sqlite3\nimport traceback\nfrom abc import ABC, abstractmethod\nfrom typing import List, Tuple, Union\nfrom urllib.parse import urlparse\n\nimport pandas as pd\nimport plotly\nimport plotly.express as px\nimport plotly.graph_objects as go\nimport requests\nimport sqlparse\n\nfrom ..exceptions import DependencyError, ImproperlyConfigured, ValidationError\nfrom ..types import TrainingPlan, TrainingPlanItem\nfrom ..utils import validate_config_path\n\n\nclass VannaBase(ABC):\n    def __init__(self, config=None):\n        if config is None:\n            config = {}\n\n        self.config = config\n        self.run_sql_is_set = False\n        self.static_documentation = \"\"\n        self.dialect = self.config.get(\"dialect\", \"SQL\")\n        self.language = self.config.get(\"language\", None)\n        self.max_tokens = self.config.get(\"max_tokens\", 14000)\n\n    def log(self, message: str, title: str = \"Info\"):\n        print(f\"{title}: {message}\")\n\n    def _response_language(self) -> str:\n        if self.language is None:\n            return \"\"\n\n        return f\"Respond in the {self.language} language.\"\n\n    def generate_sql(self, question: str, allow_llm_to_see_data=False, **kwargs) -> str:\n        \"\"\"\n        Example:\n        ```python\n        vn.generate_sql(\"What are the top 10 customers by sales?\")\n        ```\n\n        Uses the LLM to generate a SQL query that answers a question. It runs the following methods:\n\n        - [`get_similar_question_sql`][vanna.base.base.VannaBase.get_similar_question_sql]\n\n        - [`get_related_ddl`][vanna.base.base.VannaBase.get_related_ddl]\n\n        - [`get_related_documentation`][vanna.base.base.VannaBase.get_related_documentation]\n\n        - [`get_sql_prompt`][vanna.base.base.VannaBase.get_sql_prompt]\n\n        - [`submit_prompt`][vanna.base.base.VannaBase.submit_prompt]\n\n\n        Args:\n            question (str): The question to generate a SQL query for.\n            allow_llm_to_see_data (bool): Whether to allow the LLM to see the data (for the purposes of introspecting the data to generate the final SQL).\n\n        Returns:\n            str: The SQL query that answers the question.\n        \"\"\"\n        if self.config is not None:\n            initial_prompt = self.config.get(\"initial_prompt\", None)\n        else:\n            initial_prompt = None\n        question_sql_list = self.get_similar_question_sql(question, **kwargs)\n        ddl_list = self.get_related_ddl(question, **kwargs)\n        doc_list = self.get_related_documentation(question, **kwargs)\n        prompt = self.get_sql_prompt(\n            initial_prompt=initial_prompt,\n            question=question,\n            question_sql_list=question_sql_list,\n            ddl_list=ddl_list,\n            doc_list=doc_list,\n            **kwargs,\n        )\n        self.log(title=\"SQL Prompt\", message=prompt)\n        llm_response = self.submit_prompt(prompt, **kwargs)\n        self.log(title=\"LLM Response\", message=llm_response)\n\n        if \"intermediate_sql\" in llm_response:\n            if not allow_llm_to_see_data:\n                return \"The LLM is not allowed to see the data in your database. Your question requires database introspection to generate the necessary SQL. Please set allow_llm_to_see_data=True to enable this.\"\n\n            if allow_llm_to_see_data:\n                intermediate_sql = self.extract_sql(llm_response)\n\n                try:\n                    self.log(title=\"Running Intermediate SQL\", message=intermediate_sql)\n                    df = self.run_sql(intermediate_sql)\n\n                    prompt = self.get_sql_prompt(\n                        initial_prompt=initial_prompt,\n                        question=question,\n                        question_sql_list=question_sql_list,\n                        ddl_list=ddl_list,\n                        doc_list=doc_list\n                        + [\n                            f\"The following is a pandas DataFrame with the results of the intermediate SQL query {intermediate_sql}: \\n\"\n                            + df.to_markdown()\n                        ],\n                        **kwargs,\n                    )\n                    self.log(title=\"Final SQL Prompt\", message=prompt)\n                    llm_response = self.submit_prompt(prompt, **kwargs)\n                    self.log(title=\"LLM Response\", message=llm_response)\n                except Exception as e:\n                    return f\"Error running intermediate SQL: {e}\"\n\n        return self.extract_sql(llm_response)\n\n    def extract_sql(self, llm_response: str) -> str:\n        \"\"\"\n        Example:\n        ```python\n        vn.extract_sql(\"Here's the SQL query in a code block: ```sql\\nSELECT * FROM customers\\n```\")\n        ```\n\n        Extracts the SQL query from the LLM response. This is useful in case the LLM response contains other information besides the SQL query.\n        Override this function if your LLM responses need custom extraction logic.\n\n        Args:\n            llm_response (str): The LLM response.\n\n        Returns:\n            str: The extracted SQL query.\n        \"\"\"\n\n        import re\n\n        \"\"\"\n        Extracts the SQL query from the LLM response, handling various formats including:\n        - WITH clause\n        - SELECT statement\n        - CREATE TABLE AS SELECT\n        - Markdown code blocks\n        \"\"\"\n\n        # Match CREATE TABLE ... AS SELECT\n        sqls = re.findall(\n            r\"\\bCREATE\\s+TABLE\\b.*?\\bAS\\b.*?;\", llm_response, re.DOTALL | re.IGNORECASE\n        )\n        if sqls:\n            sql = sqls[-1]\n            self.log(title=\"Extracted SQL\", message=f\"{sql}\")\n            return sql\n\n        # Match WITH clause (CTEs)\n        sqls = re.findall(r\"\\bWITH\\b .*?;\", llm_response, re.DOTALL | re.IGNORECASE)\n        if sqls:\n            sql = sqls[-1]\n            self.log(title=\"Extracted SQL\", message=f\"{sql}\")\n            return sql\n\n        # Match SELECT ... ;\n        sqls = re.findall(r\"\\bSELECT\\b .*?;\", llm_response, re.DOTALL | re.IGNORECASE)\n        if sqls:\n            sql = sqls[-1]\n            self.log(title=\"Extracted SQL\", message=f\"{sql}\")\n            return sql\n\n        # Match ```sql ... ``` blocks\n        sqls = re.findall(\n            r\"```sql\\s*\\n(.*?)```\", llm_response, re.DOTALL | re.IGNORECASE\n        )\n        if sqls:\n            sql = sqls[-1].strip()\n            self.log(title=\"Extracted SQL\", message=f\"{sql}\")\n            return sql\n\n        # Match any ``` ... ``` code blocks\n        sqls = re.findall(r\"```(.*?)```\", llm_response, re.DOTALL | re.IGNORECASE)\n        if sqls:\n            sql = sqls[-1].strip()\n            self.log(title=\"Extracted SQL\", message=f\"{sql}\")\n            return sql\n\n        return llm_response\n\n    def is_sql_valid(self, sql: str) -> bool:\n        \"\"\"\n        Example:\n        ```python\n        vn.is_sql_valid(\"SELECT * FROM customers\")\n        ```\n        Checks if the SQL query is valid. This is usually used to check if we should run the SQL query or not.\n        By default it checks if the SQL query is a SELECT statement. You can override this method to enable running other types of SQL queries.\n\n        Args:\n            sql (str): The SQL query to check.\n\n        Returns:\n            bool: True if the SQL query is valid, False otherwise.\n        \"\"\"\n\n        parsed = sqlparse.parse(sql)\n\n        for statement in parsed:\n            if statement.get_type() == \"SELECT\":\n                return True\n\n        return False\n\n    def should_generate_chart(self, df: pd.DataFrame) -> bool:\n        \"\"\"\n        Example:\n        ```python\n        vn.should_generate_chart(df)\n        ```\n\n        Checks if a chart should be generated for the given DataFrame. By default, it checks if the DataFrame has more than one row and has numerical columns.\n        You can override this method to customize the logic for generating charts.\n\n        Args:\n            df (pd.DataFrame): The DataFrame to check.\n\n        Returns:\n            bool: True if a chart should be generated, False otherwise.\n        \"\"\"\n\n        if len(df) > 1 and df.select_dtypes(include=[\"number\"]).shape[1] > 0:\n            return True\n\n        return False\n\n    def generate_rewritten_question(\n        self, last_question: str, new_question: str, **kwargs\n    ) -> str:\n        \"\"\"\n        **Example:**\n        ```python\n        rewritten_question = vn.generate_rewritten_question(\"Who are the top 5 customers by sales?\", \"Show me their email addresses\")\n        ```\n\n        Generate a rewritten question by combining the last question and the new question if they are related. If the new question is self-contained and not related to the last question, return the new question.\n\n        Args:\n            last_question (str): The previous question that was asked.\n            new_question (str): The new question to be combined with the last question.\n            **kwargs: Additional keyword arguments.\n\n        Returns:\n            str: The combined question if related, otherwise the new question.\n        \"\"\"\n        if last_question is None:\n            return new_question\n\n        prompt = [\n            self.system_message(\n                \"Your goal is to combine a sequence of questions into a singular question if they are related. If the second question does not relate to the first question and is fully self-contained, return the second question. Return just the new combined question with no additional explanations. The question should theoretically be answerable with a single SQL statement.\"\n            ),\n            self.user_message(\n                \"First question: \"\n                + last_question\n                + \"\\nSecond question: \"\n                + new_question\n            ),\n        ]\n\n        return self.submit_prompt(prompt=prompt, **kwargs)\n\n    def generate_followup_questions(\n        self, question: str, sql: str, df: pd.DataFrame, n_questions: int = 5, **kwargs\n    ) -> list:\n        \"\"\"\n        **Example:**\n        ```python\n        vn.generate_followup_questions(\"What are the top 10 customers by sales?\", sql, df)\n        ```\n\n        Generate a list of followup questions that you can ask Vanna.AI.\n\n        Args:\n            question (str): The question that was asked.\n            sql (str): The LLM-generated SQL query.\n            df (pd.DataFrame): The results of the SQL query.\n            n_questions (int): Number of follow-up questions to generate.\n\n        Returns:\n            list: A list of followup questions that you can ask Vanna.AI.\n        \"\"\"\n\n        message_log = [\n            self.system_message(\n                f\"You are a helpful data assistant. The user asked the question: '{question}'\\n\\nThe SQL query for this question was: {sql}\\n\\nThe following is a pandas DataFrame with the results of the query: \\n{df.head(25).to_markdown()}\\n\\n\"\n            ),\n            self.user_message(\n                f\"Generate a list of {n_questions} followup questions that the user might ask about this data. Respond with a list of questions, one per line. Do not answer with any explanations -- just the questions. Remember that there should be an unambiguous SQL query that can be generated from the question. Prefer questions that are answerable outside of the context of this conversation. Prefer questions that are slight modifications of the SQL query that was generated that allow digging deeper into the data. Each question will be turned into a button that the user can click to generate a new SQL query so don't use 'example' type questions. Each question must have a one-to-one correspondence with an instantiated SQL query.\"\n                + self._response_language()\n            ),\n        ]\n\n        llm_response = self.submit_prompt(message_log, **kwargs)\n\n        numbers_removed = re.sub(r\"^\\d+\\.\\s*\", \"\", llm_response, flags=re.MULTILINE)\n        return numbers_removed.split(\"\\n\")\n\n    def generate_questions(self, **kwargs) -> List[str]:\n        \"\"\"\n        **Example:**\n        ```python\n        vn.generate_questions()\n        ```\n\n        Generate a list of questions that you can ask Vanna.AI.\n        \"\"\"\n        question_sql = self.get_similar_question_sql(question=\"\", **kwargs)\n\n        return [q[\"question\"] for q in question_sql]\n\n    def generate_summary(self, question: str, df: pd.DataFrame, **kwargs) -> str:\n        \"\"\"\n        **Example:**\n        ```python\n        vn.generate_summary(\"What are the top 10 customers by sales?\", df)\n        ```\n\n        Generate a summary of the results of a SQL query.\n\n        Args:\n            question (str): The question that was asked.\n            df (pd.DataFrame): The results of the SQL query.\n\n        Returns:\n            str: The summary of the results of the SQL query.\n        \"\"\"\n\n        message_log = [\n            self.system_message(\n                f\"You are a helpful data assistant. The user asked the question: '{question}'\\n\\nThe following is a pandas DataFrame with the results of the query: \\n{df.to_markdown()}\\n\\n\"\n            ),\n            self.user_message(\n                \"Briefly summarize the data based on the question that was asked. Do not respond with any additional explanation beyond the summary.\"\n                + self._response_language()\n            ),\n        ]\n\n        summary = self.submit_prompt(message_log, **kwargs)\n\n        return summary\n\n    # ----------------- Use Any Embeddings API ----------------- #\n    @abstractmethod\n    def generate_embedding(self, data: str, **kwargs) -> List[float]:\n        pass\n\n    # ----------------- Use Any Database to Store and Retrieve Context ----------------- #\n    @abstractmethod\n    def get_similar_question_sql(self, question: str, **kwargs) -> list:\n        \"\"\"\n        This method is used to get similar questions and their corresponding SQL statements.\n\n        Args:\n            question (str): The question to get similar questions and their corresponding SQL statements for.\n\n        Returns:\n            list: A list of similar questions and their corresponding SQL statements.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_related_ddl(self, question: str, **kwargs) -> list:\n        \"\"\"\n        This method is used to get related DDL statements to a question.\n\n        Args:\n            question (str): The question to get related DDL statements for.\n\n        Returns:\n            list: A list of related DDL statements.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_related_documentation(self, question: str, **kwargs) -> list:\n        \"\"\"\n        This method is used to get related documentation to a question.\n\n        Args:\n            question (str): The question to get related documentation for.\n\n        Returns:\n            list: A list of related documentation.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def add_question_sql(self, question: str, sql: str, **kwargs) -> str:\n        \"\"\"\n        This method is used to add a question and its corresponding SQL query to the training data.\n\n        Args:\n            question (str): The question to add.\n            sql (str): The SQL query to add.\n\n        Returns:\n            str: The ID of the training data that was added.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def add_ddl(self, ddl: str, **kwargs) -> str:\n        \"\"\"\n        This method is used to add a DDL statement to the training data.\n\n        Args:\n            ddl (str): The DDL statement to add.\n\n        Returns:\n            str: The ID of the training data that was added.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def add_documentation(self, documentation: str, **kwargs) -> str:\n        \"\"\"\n        This method is used to add documentation to the training data.\n\n        Args:\n            documentation (str): The documentation to add.\n\n        Returns:\n            str: The ID of the training data that was added.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_training_data(self, **kwargs) -> pd.DataFrame:\n        \"\"\"\n        Example:\n        ```python\n        vn.get_training_data()\n        ```\n\n        This method is used to get all the training data from the retrieval layer.\n\n        Returns:\n            pd.DataFrame: The training data.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def remove_training_data(self, id: str, **kwargs) -> bool:\n        \"\"\"\n        Example:\n        ```python\n        vn.remove_training_data(id=\"123-ddl\")\n        ```\n\n        This method is used to remove training data from the retrieval layer.\n\n        Args:\n            id (str): The ID of the training data to remove.\n\n        Returns:\n            bool: True if the training data was removed, False otherwise.\n        \"\"\"\n        pass\n\n    # ----------------- Use Any Language Model API ----------------- #\n\n    @abstractmethod\n    def system_message(self, message: str) -> any:\n        pass\n\n    @abstractmethod\n    def user_message(self, message: str) -> any:\n        pass\n\n    @abstractmethod\n    def assistant_message(self, message: str) -> any:\n        pass\n\n    def str_to_approx_token_count(self, string: str) -> int:\n        return len(string) / 4\n\n    def add_ddl_to_prompt(\n        self, initial_prompt: str, ddl_list: list[str], max_tokens: int = 14000\n    ) -> str:\n        if len(ddl_list) > 0:\n            initial_prompt += \"\\n===Tables \\n\"\n\n            for ddl in ddl_list:\n                if (\n                    self.str_to_approx_token_count(initial_prompt)\n                    + self.str_to_approx_token_count(ddl)\n                    < max_tokens\n                ):\n                    initial_prompt += f\"{ddl}\\n\\n\"\n\n        return initial_prompt\n\n    def add_documentation_to_prompt(\n        self,\n        initial_prompt: str,\n        documentation_list: list[str],\n        max_tokens: int = 14000,\n    ) -> str:\n        if len(documentation_list) > 0:\n            initial_prompt += \"\\n===Additional Context \\n\\n\"\n\n            for documentation in documentation_list:\n                if (\n                    self.str_to_approx_token_count(initial_prompt)\n                    + self.str_to_approx_token_count(documentation)\n                    < max_tokens\n                ):\n                    initial_prompt += f\"{documentation}\\n\\n\"\n\n        return initial_prompt\n\n    def add_sql_to_prompt(\n        self, initial_prompt: str, sql_list: list[str], max_tokens: int = 14000\n    ) -> str:\n        if len(sql_list) > 0:\n            initial_prompt += \"\\n===Question-SQL Pairs\\n\\n\"\n\n            for question in sql_list:\n                if (\n                    self.str_to_approx_token_count(initial_prompt)\n                    + self.str_to_approx_token_count(question[\"sql\"])\n                    < max_tokens\n                ):\n                    initial_prompt += f\"{question['question']}\\n{question['sql']}\\n\\n\"\n\n        return initial_prompt\n\n    def get_sql_prompt(\n        self,\n        initial_prompt: str,\n        question: str,\n        question_sql_list: list,\n        ddl_list: list,\n        doc_list: list,\n        **kwargs,\n    ):\n        \"\"\"\n        Example:\n        ```python\n        vn.get_sql_prompt(\n            question=\"What are the top 10 customers by sales?\",\n            question_sql_list=[{\"question\": \"What are the top 10 customers by sales?\", \"sql\": \"SELECT * FROM customers ORDER BY sales DESC LIMIT 10\"}],\n            ddl_list=[\"CREATE TABLE customers (id INT, name TEXT, sales DECIMAL)\"],\n            doc_list=[\"The customers table contains information about customers and their sales.\"],\n        )\n\n        ```\n\n        This method is used to generate a prompt for the LLM to generate SQL.\n\n        Args:\n            question (str): The question to generate SQL for.\n            question_sql_list (list): A list of questions and their corresponding SQL statements.\n            ddl_list (list): A list of DDL statements.\n            doc_list (list): A list of documentation.\n\n        Returns:\n            any: The prompt for the LLM to generate SQL.\n        \"\"\"\n\n        if initial_prompt is None:\n            initial_prompt = (\n                f\"You are a {self.dialect} expert. \"\n                + \"Please help to generate a SQL query to answer the question. Your response should ONLY be based on the given context and follow the response guidelines and format instructions. \"\n            )\n\n        initial_prompt = self.add_ddl_to_prompt(\n            initial_prompt, ddl_list, max_tokens=self.max_tokens\n        )\n\n        if self.static_documentation != \"\":\n            doc_list.append(self.static_documentation)\n\n        initial_prompt = self.add_documentation_to_prompt(\n            initial_prompt, doc_list, max_tokens=self.max_tokens\n        )\n\n        initial_prompt += (\n            \"===Response Guidelines \\n\"\n            \"1. If the provided context is sufficient, please generate a valid SQL query without any explanations for the question. \\n\"\n            \"2. If the provided context is almost sufficient but requires knowledge of a specific string in a particular column, please generate an intermediate SQL query to find the distinct strings in that column. Prepend the query with a comment saying intermediate_sql \\n\"\n            \"3. If the provided context is insufficient, please explain why it can't be generated. \\n\"\n            \"4. Please use the most relevant table(s). \\n\"\n            \"5. If the question has been asked and answered before, please repeat the answer exactly as it was given before. \\n\"\n            f\"6. Ensure that the output SQL is {self.dialect}-compliant and executable, and free of syntax errors. \\n\"\n        )\n\n        message_log = [self.system_message(initial_prompt)]\n\n        for example in question_sql_list:\n            if example is None:\n                print(\"example is None\")\n            else:\n                if example is not None and \"question\" in example and \"sql\" in example:\n                    message_log.append(self.user_message(example[\"question\"]))\n                    message_log.append(self.assistant_message(example[\"sql\"]))\n\n        message_log.append(self.user_message(question))\n\n        return message_log\n\n    def get_followup_questions_prompt(\n        self,\n        question: str,\n        question_sql_list: list,\n        ddl_list: list,\n        doc_list: list,\n        **kwargs,\n    ) -> list:\n        initial_prompt = f\"The user initially asked the question: '{question}': \\n\\n\"\n\n        initial_prompt = self.add_ddl_to_prompt(\n            initial_prompt, ddl_list, max_tokens=self.max_tokens\n        )\n\n        initial_prompt = self.add_documentation_to_prompt(\n            initial_prompt, doc_list, max_tokens=self.max_tokens\n        )\n\n        initial_prompt = self.add_sql_to_prompt(\n            initial_prompt, question_sql_list, max_tokens=self.max_tokens\n        )\n\n        message_log = [self.system_message(initial_prompt)]\n        message_log.append(\n            self.user_message(\n                \"Generate a list of followup questions that the user might ask about this data. Respond with a list of questions, one per line. Do not answer with any explanations -- just the questions.\"\n            )\n        )\n\n        return message_log\n\n    @abstractmethod\n    def submit_prompt(self, prompt, **kwargs) -> str:\n        \"\"\"\n        Example:\n        ```python\n        vn.submit_prompt(\n            [\n                vn.system_message(\"The user will give you SQL and you will try to guess what the business question this query is answering. Return just the question without any additional explanation. Do not reference the table name in the question.\"),\n                vn.user_message(\"What are the top 10 customers by sales?\"),\n            ]\n        )\n        ```\n\n        This method is used to submit a prompt to the LLM.\n\n        Args:\n            prompt (any): The prompt to submit to the LLM.\n\n        Returns:\n            str: The response from the LLM.\n        \"\"\"\n        pass\n\n    def generate_question(self, sql: str, **kwargs) -> str:\n        response = self.submit_prompt(\n            [\n                self.system_message(\n                    \"The user will give you SQL and you will try to guess what the business question this query is answering. Return just the question without any additional explanation. Do not reference the table name in the question.\"\n                ),\n                self.user_message(sql),\n            ],\n            **kwargs,\n        )\n\n        return response\n\n    def _extract_python_code(self, markdown_string: str) -> str:\n        # Strip whitespace to avoid indentation errors in LLM-generated code\n        markdown_string = markdown_string.strip()\n\n        # Regex pattern to match Python code blocks\n        pattern = r\"```[\\w\\s]*python\\n([\\s\\S]*?)```|```([\\s\\S]*?)```\"\n\n        # Find all matches in the markdown string\n        matches = re.findall(pattern, markdown_string, re.IGNORECASE)\n\n        # Extract the Python code from the matches\n        python_code = []\n        for match in matches:\n            python = match[0] if match[0] else match[1]\n            python_code.append(python.strip())\n\n        if len(python_code) == 0:\n            return markdown_string\n\n        return python_code[0]\n\n    def _sanitize_plotly_code(self, raw_plotly_code: str) -> str:\n        # Remove the fig.show() statement from the plotly code\n        plotly_code = raw_plotly_code.replace(\"fig.show()\", \"\")\n\n        return plotly_code\n\n    def generate_plotly_code(\n        self, question: str = None, sql: str = None, df_metadata: str = None, **kwargs\n    ) -> str:\n        if question is not None:\n            system_msg = f\"The following is a pandas DataFrame that contains the results of the query that answers the question the user asked: '{question}'\"\n        else:\n            system_msg = \"The following is a pandas DataFrame \"\n\n        if sql is not None:\n            system_msg += f\"\\n\\nThe DataFrame was produced using this query: {sql}\\n\\n\"\n\n        system_msg += f\"The following is information about the resulting pandas DataFrame 'df': \\n{df_metadata}\"\n\n        message_log = [\n            self.system_message(system_msg),\n            self.user_message(\n                \"Can you generate the Python plotly code to chart the results of the dataframe? Assume the data is in a pandas dataframe called 'df'. If there is only one value in the dataframe, use an Indicator. Respond with only Python code. Do not answer with any explanations -- just the code.\"\n            ),\n        ]\n\n        plotly_code = self.submit_prompt(message_log, kwargs=kwargs)\n\n        return self._sanitize_plotly_code(self._extract_python_code(plotly_code))\n\n    # ----------------- Connect to Any Database to run the Generated SQL ----------------- #\n\n    def connect_to_snowflake(\n        self,\n        account: str,\n        username: str,\n        password: str,\n        database: str,\n        role: Union[str, None] = None,\n        warehouse: Union[str, None] = None,\n        **kwargs,\n    ):\n        try:\n            snowflake = __import__(\"snowflake.connector\")\n        except ImportError:\n            raise DependencyError(\n                \"You need to install required dependencies to execute this method, run command:\"\n                \" \\npip install vanna[snowflake]\"\n            )\n\n        if username == \"my-username\":\n            username_env = os.getenv(\"SNOWFLAKE_USERNAME\")\n\n            if username_env is not None:\n                username = username_env\n            else:\n                raise ImproperlyConfigured(\"Please set your Snowflake username.\")\n\n        if password == \"mypassword\":\n            password_env = os.getenv(\"SNOWFLAKE_PASSWORD\")\n\n            if password_env is not None:\n                password = password_env\n            else:\n                raise ImproperlyConfigured(\"Please set your Snowflake password.\")\n\n        if account == \"my-account\":\n            account_env = os.getenv(\"SNOWFLAKE_ACCOUNT\")\n\n            if account_env is not None:\n                account = account_env\n            else:\n                raise ImproperlyConfigured(\"Please set your Snowflake account.\")\n\n        if database == \"my-database\":\n            database_env = os.getenv(\"SNOWFLAKE_DATABASE\")\n\n            if database_env is not None:\n                database = database_env\n            else:\n                raise ImproperlyConfigured(\"Please set your Snowflake database.\")\n\n        conn = snowflake.connector.connect(\n            user=username,\n            password=password,\n            account=account,\n            database=database,\n            client_session_keep_alive=True,\n            **kwargs,\n        )\n\n        def run_sql_snowflake(sql: str) -> pd.DataFrame:\n            cs = conn.cursor()\n\n            if role is not None:\n                cs.execute(f\"USE ROLE {role}\")\n\n            if warehouse is not None:\n                cs.execute(f\"USE WAREHOUSE {warehouse}\")\n            cs.execute(f\"USE DATABASE {database}\")\n\n            cur = cs.execute(sql)\n\n            results = cur.fetchall()\n\n            # Create a pandas dataframe from the results\n            df = pd.DataFrame(results, columns=[desc[0] for desc in cur.description])\n\n            return df\n\n        self.dialect = \"Snowflake SQL\"\n        self.run_sql = run_sql_snowflake\n        self.run_sql_is_set = True\n\n    def connect_to_sqlite(self, url: str, check_same_thread: bool = False, **kwargs):\n        \"\"\"\n        Connect to a SQLite database. This is just a helper function to set [`vn.run_sql`][vanna.base.base.VannaBase.run_sql]\n\n        Args:\n            url (str): The URL of the database to connect to.\n            check_same_thread (str): Allow the connection may be accessed in multiple threads.\n        Returns:\n            None\n        \"\"\"\n\n        # URL of the database to download\n\n        # Path to save the downloaded database\n        path = os.path.basename(urlparse(url).path)\n\n        # Download the database if it doesn't exist\n        if not os.path.exists(url):\n            response = requests.get(url)\n            response.raise_for_status()  # Check that the request was successful\n            with open(path, \"wb\") as f:\n                f.write(response.content)\n            url = path\n\n        # Connect to the database\n        conn = sqlite3.connect(url, check_same_thread=check_same_thread, **kwargs)\n\n        def run_sql_sqlite(sql: str):\n            return pd.read_sql_query(sql, conn)\n\n        self.dialect = \"SQLite\"\n        self.run_sql = run_sql_sqlite\n        self.run_sql_is_set = True\n\n    def connect_to_postgres(\n        self,\n        host: str = None,\n        dbname: str = None,\n        user: str = None,\n        password: str = None,\n        port: int = None,\n        **kwargs,\n    ):\n        \"\"\"\n        Connect to postgres using the psycopg2 connector. This is just a helper function to set [`vn.run_sql`][vanna.base.base.VannaBase.run_sql]\n        **Example:**\n        ```python\n        vn.connect_to_postgres(\n            host=\"myhost\",\n            dbname=\"mydatabase\",\n            user=\"myuser\",\n            password=\"mypassword\",\n            port=5432\n        )\n        ```\n        Args:\n            host (str): The postgres host.\n            dbname (str): The postgres database name.\n            user (str): The postgres user.\n            password (str): The postgres password.\n            port (int): The postgres Port.\n        \"\"\"\n\n        try:\n            import psycopg2\n            import psycopg2.extras\n        except ImportError:\n            raise DependencyError(\n                \"You need to install required dependencies to execute this method,\"\n                \" run command: \\npip install vanna[postgres]\"\n            )\n\n        if not host:\n            host = os.getenv(\"HOST\")\n\n        if not host:\n            raise ImproperlyConfigured(\"Please set your postgres host\")\n\n        if not dbname:\n            dbname = os.getenv(\"DATABASE\")\n\n        if not dbname:\n            raise ImproperlyConfigured(\"Please set your postgres database\")\n\n        if not user:\n            user = os.getenv(\"PG_USER\")\n\n        if not user:\n            raise ImproperlyConfigured(\"Please set your postgres user\")\n\n        if not password:\n            password = os.getenv(\"PASSWORD\")\n\n        if not password:\n            raise ImproperlyConfigured(\"Please set your postgres password\")\n\n        if not port:\n            port = os.getenv(\"PORT\")\n\n        if not port:\n            raise ImproperlyConfigured(\"Please set your postgres port\")\n\n        conn = None\n\n        try:\n            conn = psycopg2.connect(\n                host=host,\n                dbname=dbname,\n                user=user,\n                password=password,\n                port=port,\n                **kwargs,\n            )\n        except psycopg2.Error as e:\n            raise ValidationError(e)\n\n        def connect_to_db():\n            return psycopg2.connect(\n                host=host,\n                dbname=dbname,\n                user=user,\n                password=password,\n                port=port,\n                **kwargs,\n            )\n\n        def run_sql_postgres(sql: str) -> Union[pd.DataFrame, None]:\n            conn = None\n            try:\n                conn = connect_to_db()  # Initial connection attempt\n                cs = conn.cursor()\n                cs.execute(sql)\n                results = cs.fetchall()\n\n                # Create a pandas dataframe from the results\n                df = pd.DataFrame(results, columns=[desc[0] for desc in cs.description])\n                return df\n\n            except psycopg2.InterfaceError as e:\n                # Attempt to reconnect and retry the operation\n                if conn:\n                    conn.close()  # Ensure any existing connection is closed\n                conn = connect_to_db()\n                cs = conn.cursor()\n                cs.execute(sql)\n                results = cs.fetchall()\n\n                # Create a pandas dataframe from the results\n                df = pd.DataFrame(results, columns=[desc[0] for desc in cs.description])\n                return df\n\n            except psycopg2.Error as e:\n                if conn:\n                    conn.rollback()\n                    raise ValidationError(e)\n\n            except Exception as e:\n                conn.rollback()\n                raise e\n\n        self.dialect = \"PostgreSQL\"\n        self.run_sql_is_set = True\n        self.run_sql = run_sql_postgres\n\n    def connect_to_mysql(\n        self,\n        host: str = None,\n        dbname: str = None,\n        user: str = None,\n        password: str = None,\n        port: int = None,\n        **kwargs,\n    ):\n        try:\n            import pymysql.cursors\n        except ImportError:\n            raise DependencyError(\n                \"You need to install required dependencies to execute this method,\"\n                \" run command: \\npip install PyMySQL\"\n            )\n\n        if not host:\n            host = os.getenv(\"HOST\")\n\n        if not host:\n            raise ImproperlyConfigured(\"Please set your MySQL host\")\n\n        if not dbname:\n            dbname = os.getenv(\"DATABASE\")\n\n        if not dbname:\n            raise ImproperlyConfigured(\"Please set your MySQL database\")\n\n        if not user:\n            user = os.getenv(\"USER\")\n\n        if not user:\n            raise ImproperlyConfigured(\"Please set your MySQL user\")\n\n        if not password:\n            password = os.getenv(\"PASSWORD\")\n\n        if not password:\n            raise ImproperlyConfigured(\"Please set your MySQL password\")\n\n        if not port:\n            port = os.getenv(\"PORT\")\n\n        if not port:\n            raise ImproperlyConfigured(\"Please set your MySQL port\")\n\n        conn = None\n\n        try:\n            conn = pymysql.connect(\n                host=host,\n                user=user,\n                password=password,\n                database=dbname,\n                port=port,\n                cursorclass=pymysql.cursors.DictCursor,\n                **kwargs,\n            )\n        except pymysql.Error as e:\n            raise ValidationError(e)\n\n        def run_sql_mysql(sql: str) -> Union[pd.DataFrame, None]:\n            if conn:\n                try:\n                    conn.ping(reconnect=True)\n                    cs = conn.cursor()\n                    cs.execute(sql)\n                    results = cs.fetchall()\n\n                    # Create a pandas dataframe from the results\n                    df = pd.DataFrame(\n                        results, columns=[desc[0] for desc in cs.description]\n                    )\n                    return df\n\n                except pymysql.Error as e:\n                    conn.rollback()\n                    raise ValidationError(e)\n\n                except Exception as e:\n                    conn.rollback()\n                    raise e\n\n        self.run_sql_is_set = True\n        self.run_sql = run_sql_mysql\n\n    def connect_to_clickhouse(\n        self,\n        host: str = None,\n        dbname: str = None,\n        user: str = None,\n        password: str = None,\n        port: int = None,\n        **kwargs,\n    ):\n        try:\n            import clickhouse_connect\n        except ImportError:\n            raise DependencyError(\n                \"You need to install required dependencies to execute this method,\"\n                \" run command: \\npip install clickhouse_connect\"\n            )\n\n        if not host:\n            host = os.getenv(\"HOST\")\n\n        if not host:\n            raise ImproperlyConfigured(\"Please set your ClickHouse host\")\n\n        if not dbname:\n            dbname = os.getenv(\"DATABASE\")\n\n        if not dbname:\n            raise ImproperlyConfigured(\"Please set your ClickHouse database\")\n\n        if not user:\n            user = os.getenv(\"USER\")\n\n        if not user:\n            raise ImproperlyConfigured(\"Please set your ClickHouse user\")\n\n        if not password:\n            password = os.getenv(\"PASSWORD\")\n\n        if not password:\n            raise ImproperlyConfigured(\"Please set your ClickHouse password\")\n\n        if not port:\n            port = os.getenv(\"PORT\")\n\n        if not port:\n            raise ImproperlyConfigured(\"Please set your ClickHouse port\")\n\n        conn = None\n\n        try:\n            conn = clickhouse_connect.get_client(\n                host=host,\n                port=port,\n                username=user,\n                password=password,\n                database=dbname,\n                **kwargs,\n            )\n            print(conn)\n        except Exception as e:\n            raise ValidationError(e)\n\n        def run_sql_clickhouse(sql: str) -> Union[pd.DataFrame, None]:\n            if conn:\n                try:\n                    result = conn.query(sql)\n                    results = result.result_rows\n\n                    # Create a pandas dataframe from the results\n                    df = pd.DataFrame(results, columns=result.column_names)\n                    return df\n\n                except Exception as e:\n                    raise e\n\n        self.run_sql_is_set = True\n        self.run_sql = run_sql_clickhouse\n\n    def connect_to_oracle(\n        self, user: str = None, password: str = None, dsn: str = None, **kwargs\n    ):\n        \"\"\"\n        Connect to an Oracle db using oracledb package. This is just a helper function to set [`vn.run_sql`][vanna.base.base.VannaBase.run_sql]\n        **Example:**\n        ```python\n        vn.connect_to_oracle(\n        user=\"username\",\n        password=\"password\",\n        dsn=\"host:port/sid\",\n        )\n        ```\n        Args:\n            USER (str): Oracle db user name.\n            PASSWORD (str): Oracle db user password.\n            DSN (str): Oracle db host ip - host:port/sid.\n        \"\"\"\n\n        try:\n            import oracledb\n        except ImportError:\n            raise DependencyError(\n                \"You need to install required dependencies to execute this method,\"\n                \" run command: \\npip install oracledb\"\n            )\n\n        if not dsn:\n            dsn = os.getenv(\"DSN\")\n\n        if not dsn:\n            raise ImproperlyConfigured(\n                \"Please set your Oracle dsn which should include host:port/sid\"\n            )\n\n        if not user:\n            user = os.getenv(\"USER\")\n\n        if not user:\n            raise ImproperlyConfigured(\"Please set your Oracle db user\")\n\n        if not password:\n            password = os.getenv(\"PASSWORD\")\n\n        if not password:\n            raise ImproperlyConfigured(\"Please set your Oracle db password\")\n\n        conn = None\n\n        try:\n            conn = oracledb.connect(user=user, password=password, dsn=dsn, **kwargs)\n        except oracledb.Error as e:\n            raise ValidationError(e)\n\n        def run_sql_oracle(sql: str) -> Union[pd.DataFrame, None]:\n            if conn:\n                try:\n                    sql = sql.rstrip()\n                    if sql.endswith(\n                        \";\"\n                    ):  # fix for a known problem with Oracle db where an extra ; will cause an error.\n                        sql = sql[:-1]\n\n                    cs = conn.cursor()\n                    cs.execute(sql)\n                    results = cs.fetchall()\n\n                    # Create a pandas dataframe from the results\n                    df = pd.DataFrame(\n                        results, columns=[desc[0] for desc in cs.description]\n                    )\n                    return df\n\n                except oracledb.Error as e:\n                    conn.rollback()\n                    raise ValidationError(e)\n\n                except Exception as e:\n                    conn.rollback()\n                    raise e\n\n        self.run_sql_is_set = True\n        self.run_sql = run_sql_oracle\n\n    def connect_to_bigquery(\n        self, cred_file_path: str = None, project_id: str = None, **kwargs\n    ):\n        \"\"\"\n        Connect to gcs using the bigquery connector. This is just a helper function to set [`vn.run_sql`][vanna.base.base.VannaBase.run_sql]\n        **Example:**\n        ```python\n        vn.connect_to_bigquery(\n            project_id=\"myprojectid\",\n            cred_file_path=\"path/to/credentials.json\",\n        )\n        ```\n        Args:\n            project_id (str): The gcs project id.\n            cred_file_path (str): The gcs credential file path\n        \"\"\"\n\n        try:\n            from google.api_core.exceptions import GoogleAPIError\n            from google.cloud import bigquery\n            from google.oauth2 import service_account\n        except ImportError:\n            raise DependencyError(\n                \"You need to install required dependencies to execute this method, run command:\"\n                \" \\npip install vanna[bigquery]\"\n            )\n\n        if not project_id:\n            project_id = os.getenv(\"PROJECT_ID\")\n\n        if not project_id:\n            raise ImproperlyConfigured(\"Please set your Google Cloud Project ID.\")\n\n        import sys\n\n        if \"google.colab\" in sys.modules:\n            try:\n                from google.colab import auth\n\n                auth.authenticate_user()\n            except Exception as e:\n                raise ImproperlyConfigured(e)\n        else:\n            print(\"Not using Google Colab.\")\n\n        conn = None\n\n        if not cred_file_path:\n            try:\n                conn = bigquery.Client(project=project_id)\n            except Exception:\n                print(\"Could not found any google cloud implicit credentials\")\n        else:\n            # Validate file path and pemissions\n            validate_config_path(cred_file_path)\n\n        if not conn:\n            with open(cred_file_path, \"r\") as f:\n                credentials = service_account.Credentials.from_service_account_info(\n                    json.loads(f.read()),\n                    scopes=[\"https://www.googleapis.com/auth/cloud-platform\"],\n                )\n\n            try:\n                conn = bigquery.Client(\n                    project=project_id, credentials=credentials, **kwargs\n                )\n            except Exception:\n                raise ImproperlyConfigured(\n                    \"Could not connect to bigquery please correct credentials\"\n                )\n\n        def run_sql_bigquery(sql: str) -> Union[pd.DataFrame, None]:\n            if conn:\n                job = conn.query(sql)\n                df = job.result().to_dataframe()\n                return df\n            return None\n\n        self.dialect = \"BigQuery SQL\"\n        self.run_sql_is_set = True\n        self.run_sql = run_sql_bigquery\n\n    def connect_to_duckdb(self, url: str, init_sql: str = None, **kwargs):\n        \"\"\"\n        Connect to a DuckDB database. This is just a helper function to set [`vn.run_sql`][vanna.base.base.VannaBase.run_sql]\n\n        Args:\n            url (str): The URL of the database to connect to. Use :memory: to create an in-memory database. Use md: or motherduck: to use the MotherDuck database.\n            init_sql (str, optional): SQL to run when connecting to the database. Defaults to None.\n\n        Returns:\n            None\n        \"\"\"\n        try:\n            import duckdb\n        except ImportError:\n            raise DependencyError(\n                \"You need to install required dependencies to execute this method,\"\n                \" run command: \\npip install vanna[duckdb]\"\n            )\n        # URL of the database to download\n        if url == \":memory:\" or url == \"\":\n            path = \":memory:\"\n        else:\n            # Path to save the downloaded database\n            print(os.path.exists(url))\n            if os.path.exists(url):\n                path = url\n            elif url.startswith(\"md\") or url.startswith(\"motherduck\"):\n                path = url\n            else:\n                path = os.path.basename(urlparse(url).path)\n                # Download the database if it doesn't exist\n                if not os.path.exists(path):\n                    response = requests.get(url)\n                    response.raise_for_status()  # Check that the request was successful\n                    with open(path, \"wb\") as f:\n                        f.write(response.content)\n\n        # Connect to the database\n        conn = duckdb.connect(path, **kwargs)\n        if init_sql:\n            conn.query(init_sql)\n\n        def run_sql_duckdb(sql: str):\n            return conn.query(sql).to_df()\n\n        self.dialect = \"DuckDB SQL\"\n        self.run_sql = run_sql_duckdb\n        self.run_sql_is_set = True\n\n    def connect_to_mssql(self, odbc_conn_str: str, **kwargs):\n        \"\"\"\n        Connect to a Microsoft SQL Server database. This is just a helper function to set [`vn.run_sql`][vanna.base.base.VannaBase.run_sql]\n\n        Args:\n            odbc_conn_str (str): The ODBC connection string.\n\n        Returns:\n            None\n        \"\"\"\n        try:\n            import pyodbc\n        except ImportError:\n            raise DependencyError(\n                \"You need to install required dependencies to execute this method,\"\n                \" run command: pip install pyodbc\"\n            )\n\n        try:\n            import sqlalchemy as sa\n            from sqlalchemy.engine import URL\n        except ImportError:\n            raise DependencyError(\n                \"You need to install required dependencies to execute this method,\"\n                \" run command: pip install sqlalchemy\"\n            )\n\n        connection_url = URL.create(\n            \"mssql+pyodbc\", query={\"odbc_connect\": odbc_conn_str}\n        )\n\n        from sqlalchemy import create_engine\n\n        engine = create_engine(connection_url, **kwargs)\n\n        def run_sql_mssql(sql: str):\n            # Execute the SQL statement and return the result as a pandas DataFrame\n            with engine.begin() as conn:\n                df = pd.read_sql_query(sa.text(sql), conn)\n                conn.close()\n                return df\n\n            raise Exception(\"Couldn't run sql\")\n\n        self.dialect = \"T-SQL / Microsoft SQL Server\"\n        self.run_sql = run_sql_mssql\n        self.run_sql_is_set = True\n\n    def connect_to_presto(\n        self,\n        host: str,\n        catalog: str = \"hive\",\n        schema: str = \"default\",\n        user: str = None,\n        password: str = None,\n        port: int = None,\n        combined_pem_path: str = None,\n        protocol: str = \"https\",\n        requests_kwargs: dict = None,\n        **kwargs,\n    ):\n        \"\"\"\n        Connect to a Presto database using the specified parameters.\n\n        Args:\n            host (str): The host address of the Presto database.\n            catalog (str): The catalog to use in the Presto environment.\n            schema (str): The schema to use in the Presto environment.\n            user (str): The username for authentication.\n            password (str): The password for authentication.\n            port (int): The port number for the Presto connection.\n            combined_pem_path (str): The path to the combined pem file for SSL connection.\n            protocol (str): The protocol to use for the connection (default is 'https').\n            requests_kwargs (dict): Additional keyword arguments for requests.\n\n        Raises:\n            DependencyError: If required dependencies are not installed.\n            ImproperlyConfigured: If essential configuration settings are missing.\n\n        Returns:\n            None\n        \"\"\"\n        try:\n            from pyhive import presto\n        except ImportError:\n            raise DependencyError(\n                \"You need to install required dependencies to execute this method,\"\n                \" run command: \\npip install pyhive\"\n            )\n\n        if not host:\n            host = os.getenv(\"PRESTO_HOST\")\n\n        if not host:\n            raise ImproperlyConfigured(\"Please set your presto host\")\n\n        if not catalog:\n            catalog = os.getenv(\"PRESTO_CATALOG\")\n\n        if not catalog:\n            raise ImproperlyConfigured(\"Please set your presto catalog\")\n\n        if not user:\n            user = os.getenv(\"PRESTO_USER\")\n\n        if not user:\n            raise ImproperlyConfigured(\"Please set your presto user\")\n\n        if not password:\n            password = os.getenv(\"PRESTO_PASSWORD\")\n\n        if not port:\n            port = os.getenv(\"PRESTO_PORT\")\n\n        if not port:\n            raise ImproperlyConfigured(\"Please set your presto port\")\n\n        conn = None\n\n        try:\n            if requests_kwargs is None and combined_pem_path is not None:\n                # use the combined pem file to verify the SSL connection\n                requests_kwargs = {\n                    \"verify\": combined_pem_path,  # 使用转换后得到的 PEM 文件进行 SSL 验证\n                }\n            conn = presto.Connection(\n                host=host,\n                username=user,\n                password=password,\n                catalog=catalog,\n                schema=schema,\n                port=port,\n                protocol=protocol,\n                requests_kwargs=requests_kwargs,\n                **kwargs,\n            )\n        except presto.Error as e:\n            raise ValidationError(e)\n\n        def run_sql_presto(sql: str) -> Union[pd.DataFrame, None]:\n            if conn:\n                try:\n                    sql = sql.rstrip()\n                    # fix for a known problem with presto db where an extra ; will cause an error.\n                    if sql.endswith(\";\"):\n                        sql = sql[:-1]\n                    cs = conn.cursor()\n                    cs.execute(sql)\n                    results = cs.fetchall()\n\n                    # Create a pandas dataframe from the results\n                    df = pd.DataFrame(\n                        results, columns=[desc[0] for desc in cs.description]\n                    )\n                    return df\n\n                except presto.Error as e:\n                    print(e)\n                    raise ValidationError(e)\n\n                except Exception as e:\n                    print(e)\n                    raise e\n\n        self.run_sql_is_set = True\n        self.run_sql = run_sql_presto\n\n    def connect_to_hive(\n        self,\n        host: str = None,\n        dbname: str = \"default\",\n        user: str = None,\n        password: str = None,\n        port: int = None,\n        auth: str = \"CUSTOM\",\n        **kwargs,\n    ):\n        \"\"\"\n        Connect to a Hive database. This is just a helper function to set [`vn.run_sql`][vanna.base.base.VannaBase.run_sql]\n        Connect to a Hive database. This is just a helper function to set [`vn.run_sql`][vanna.base.base.VannaBase.run_sql]\n\n        Args:\n            host (str): The host of the Hive database.\n            dbname (str): The name of the database to connect to.\n            user (str): The username to use for authentication.\n            password (str): The password to use for authentication.\n            port (int): The port to use for the connection.\n            auth (str): The authentication method to use.\n\n        Returns:\n            None\n        \"\"\"\n\n        try:\n            from pyhive import hive\n        except ImportError:\n            raise DependencyError(\n                \"You need to install required dependencies to execute this method,\"\n                \" run command: \\npip install pyhive\"\n            )\n\n        if not host:\n            host = os.getenv(\"HIVE_HOST\")\n\n        if not host:\n            raise ImproperlyConfigured(\"Please set your hive host\")\n\n        if not dbname:\n            dbname = os.getenv(\"HIVE_DATABASE\")\n\n        if not dbname:\n            raise ImproperlyConfigured(\"Please set your hive database\")\n\n        if not user:\n            user = os.getenv(\"HIVE_USER\")\n\n        if not user:\n            raise ImproperlyConfigured(\"Please set your hive user\")\n\n        if not password:\n            password = os.getenv(\"HIVE_PASSWORD\")\n\n        if not port:\n            port = os.getenv(\"HIVE_PORT\")\n\n        if not port:\n            raise ImproperlyConfigured(\"Please set your hive port\")\n\n        conn = None\n\n        try:\n            conn = hive.Connection(\n                host=host,\n                username=user,\n                password=password,\n                database=dbname,\n                port=port,\n                auth=auth,\n            )\n        except hive.Error as e:\n            raise ValidationError(e)\n\n        def run_sql_hive(sql: str) -> Union[pd.DataFrame, None]:\n            if conn:\n                try:\n                    cs = conn.cursor()\n                    cs.execute(sql)\n                    results = cs.fetchall()\n\n                    # Create a pandas dataframe from the results\n                    df = pd.DataFrame(\n                        results, columns=[desc[0] for desc in cs.description]\n                    )\n                    return df\n\n                except hive.Error as e:\n                    print(e)\n                    raise ValidationError(e)\n\n                except Exception as e:\n                    print(e)\n                    raise e\n\n        self.run_sql_is_set = True\n        self.run_sql = run_sql_hive\n\n    def run_sql(self, sql: str, **kwargs) -> pd.DataFrame:\n        \"\"\"\n        Example:\n        ```python\n        vn.run_sql(\"SELECT * FROM my_table\")\n        ```\n\n        Run a SQL query on the connected database.\n\n        Args:\n            sql (str): The SQL query to run.\n\n        Returns:\n            pd.DataFrame: The results of the SQL query.\n        \"\"\"\n        raise Exception(\n            \"You need to connect to a database first by running vn.connect_to_snowflake(), vn.connect_to_postgres(), similar function, or manually set vn.run_sql\"\n        )\n\n    def ask(\n        self,\n        question: Union[str, None] = None,\n        print_results: bool = True,\n        auto_train: bool = True,\n        visualize: bool = True,  # if False, will not generate plotly code\n        allow_llm_to_see_data: bool = False,\n    ) -> Union[\n        Tuple[\n            Union[str, None],\n            Union[pd.DataFrame, None],\n            Union[plotly.graph_objs.Figure, None],\n        ],\n        None,\n    ]:\n        \"\"\"\n        **Example:**\n        ```python\n        vn.ask(\"What are the top 10 customers by sales?\")\n        ```\n\n        Ask Vanna.AI a question and get the SQL query that answers it.\n\n        Args:\n            question (str): The question to ask.\n            print_results (bool): Whether to print the results of the SQL query.\n            auto_train (bool): Whether to automatically train Vanna.AI on the question and SQL query.\n            visualize (bool): Whether to generate plotly code and display the plotly figure.\n\n        Returns:\n            Tuple[str, pd.DataFrame, plotly.graph_objs.Figure]: The SQL query, the results of the SQL query, and the plotly figure.\n        \"\"\"\n\n        if question is None:\n            question = input(\"Enter a question: \")\n\n        try:\n            sql = self.generate_sql(\n                question=question, allow_llm_to_see_data=allow_llm_to_see_data\n            )\n        except Exception as e:\n            print(e)\n            return None, None, None\n\n        if print_results:\n            try:\n                from IPython.display import Code, display\n\n                display(Code(sql))\n            except Exception as e:\n                print(sql)\n\n        if self.run_sql_is_set is False:\n            print(\"If you want to run the SQL query, connect to a database first.\")\n\n            if print_results:\n                return None\n            else:\n                return sql, None, None\n\n        try:\n            df = self.run_sql(sql)\n\n            if print_results:\n                try:\n                    display = __import__(\n                        \"IPython.display\", fromList=[\"display\"]\n                    ).display\n                    display(df)\n                except Exception as e:\n                    print(df)\n\n            if len(df) > 0 and auto_train:\n                self.add_question_sql(question=question, sql=sql)\n            # Only generate plotly code if visualize is True\n            if visualize:\n                try:\n                    plotly_code = self.generate_plotly_code(\n                        question=question,\n                        sql=sql,\n                        df_metadata=f\"Running df.dtypes gives:\\n {df.dtypes}\",\n                    )\n                    fig = self.get_plotly_figure(plotly_code=plotly_code, df=df)\n                    if print_results:\n                        try:\n                            display = __import__(\n                                \"IPython.display\", fromlist=[\"display\"]\n                            ).display\n                            Image = __import__(\n                                \"IPython.display\", fromlist=[\"Image\"]\n                            ).Image\n                            img_bytes = fig.to_image(format=\"png\", scale=2)\n                            display(Image(img_bytes))\n                        except Exception as e:\n                            fig.show()\n                except Exception as e:\n                    # Print stack trace\n                    traceback.print_stack()\n                    traceback.print_exc()\n                    print(\"Couldn't run plotly code: \", e)\n                    if print_results:\n                        return None\n                    else:\n                        return sql, df, None\n            else:\n                return sql, df, None\n\n        except Exception as e:\n            print(\"Couldn't run sql: \", e)\n            if print_results:\n                return None\n            else:\n                return sql, None, None\n        return sql, df, fig\n\n    def train(\n        self,\n        question: str = None,\n        sql: str = None,\n        ddl: str = None,\n        documentation: str = None,\n        plan: TrainingPlan = None,\n    ) -> str:\n        \"\"\"\n        **Example:**\n        ```python\n        vn.train()\n        ```\n\n        Train Vanna.AI on a question and its corresponding SQL query.\n        If you call it with no arguments, it will check if you connected to a database and it will attempt to train on the metadata of that database.\n        If you call it with the sql argument, it's equivalent to [`vn.add_question_sql()`][vanna.base.base.VannaBase.add_question_sql].\n        If you call it with the ddl argument, it's equivalent to [`vn.add_ddl()`][vanna.base.base.VannaBase.add_ddl].\n        If you call it with the documentation argument, it's equivalent to [`vn.add_documentation()`][vanna.base.base.VannaBase.add_documentation].\n        Additionally, you can pass a [`TrainingPlan`][vanna.types.TrainingPlan] object. Get a training plan with [`vn.get_training_plan_generic()`][vanna.base.base.VannaBase.get_training_plan_generic].\n\n        Args:\n            question (str): The question to train on.\n            sql (str): The SQL query to train on.\n            ddl (str):  The DDL statement.\n            documentation (str): The documentation to train on.\n            plan (TrainingPlan): The training plan to train on.\n        \"\"\"\n\n        if question and not sql:\n            raise ValidationError(\"Please also provide a SQL query\")\n\n        if documentation:\n            print(\"Adding documentation....\")\n            return self.add_documentation(documentation)\n\n        if sql:\n            if question is None:\n                question = self.generate_question(sql)\n                print(\"Question generated with sql:\", question, \"\\nAdding SQL...\")\n            return self.add_question_sql(question=question, sql=sql)\n\n        if ddl:\n            print(\"Adding ddl:\", ddl)\n            return self.add_ddl(ddl)\n\n        if plan:\n            for item in plan._plan:\n                if item.item_type == TrainingPlanItem.ITEM_TYPE_DDL:\n                    self.add_ddl(item.item_value)\n                elif item.item_type == TrainingPlanItem.ITEM_TYPE_IS:\n                    self.add_documentation(item.item_value)\n                elif item.item_type == TrainingPlanItem.ITEM_TYPE_SQL:\n                    self.add_question_sql(question=item.item_name, sql=item.item_value)\n\n    def _get_databases(self) -> List[str]:\n        try:\n            print(\"Trying INFORMATION_SCHEMA.DATABASES\")\n            df_databases = self.run_sql(\"SELECT * FROM INFORMATION_SCHEMA.DATABASES\")\n        except Exception as e:\n            print(e)\n            try:\n                print(\"Trying SHOW DATABASES\")\n                df_databases = self.run_sql(\"SHOW DATABASES\")\n            except Exception as e:\n                print(e)\n                return []\n\n        return df_databases[\"DATABASE_NAME\"].unique().tolist()\n\n    def _get_information_schema_tables(self, database: str) -> pd.DataFrame:\n        df_tables = self.run_sql(f\"SELECT * FROM {database}.INFORMATION_SCHEMA.TABLES\")\n\n        return df_tables\n\n    def get_training_plan_generic(self, df) -> TrainingPlan:\n        \"\"\"\n        This method is used to generate a training plan from an information schema dataframe.\n\n        Basically what it does is breaks up INFORMATION_SCHEMA.COLUMNS into groups of table/column descriptions that can be used to pass to the LLM.\n\n        Args:\n            df (pd.DataFrame): The dataframe to generate the training plan from.\n\n        Returns:\n            TrainingPlan: The training plan.\n        \"\"\"\n        # For each of the following, we look at the df columns to see if there's a match:\n        database_column = df.columns[\n            df.columns.str.lower().str.contains(\"database\")\n            | df.columns.str.lower().str.contains(\"table_catalog\")\n        ].to_list()[0]\n        schema_column = df.columns[\n            df.columns.str.lower().str.contains(\"table_schema\")\n        ].to_list()[0]\n        table_column = df.columns[\n            df.columns.str.lower().str.contains(\"table_name\")\n        ].to_list()[0]\n        columns = [database_column, schema_column, table_column]\n        candidates = [\"column_name\", \"data_type\", \"comment\"]\n        matches = df.columns.str.lower().str.contains(\"|\".join(candidates), regex=True)\n        columns += df.columns[matches].to_list()\n\n        plan = TrainingPlan([])\n\n        for database in df[database_column].unique().tolist():\n            for schema in (\n                df.query(f'{database_column} == \"{database}\"')[schema_column]\n                .unique()\n                .tolist()\n            ):\n                for table in (\n                    df.query(\n                        f'{database_column} == \"{database}\" and {schema_column} == \"{schema}\"'\n                    )[table_column]\n                    .unique()\n                    .tolist()\n                ):\n                    df_columns_filtered_to_table = df.query(\n                        f'{database_column} == \"{database}\" and {schema_column} == \"{schema}\" and {table_column} == \"{table}\"'\n                    )\n                    doc = f\"The following columns are in the {table} table in the {database} database:\\n\\n\"\n                    doc += df_columns_filtered_to_table[columns].to_markdown()\n\n                    plan._plan.append(\n                        TrainingPlanItem(\n                            item_type=TrainingPlanItem.ITEM_TYPE_IS,\n                            item_group=f\"{database}.{schema}\",\n                            item_name=table,\n                            item_value=doc,\n                        )\n                    )\n\n        return plan\n\n    def get_training_plan_snowflake(\n        self,\n        filter_databases: Union[List[str], None] = None,\n        filter_schemas: Union[List[str], None] = None,\n        include_information_schema: bool = False,\n        use_historical_queries: bool = True,\n    ) -> TrainingPlan:\n        plan = TrainingPlan([])\n\n        if self.run_sql_is_set is False:\n            raise ImproperlyConfigured(\"Please connect to a database first.\")\n\n        if use_historical_queries:\n            try:\n                print(\"Trying query history\")\n                df_history = self.run_sql(\n                    \"\"\" select * from table(information_schema.query_history(result_limit => 5000)) order by start_time\"\"\"\n                )\n\n                df_history_filtered = df_history.query(\"ROWS_PRODUCED > 1\")\n                if filter_databases is not None:\n                    mask = (\n                        df_history_filtered[\"QUERY_TEXT\"]\n                        .str.lower()\n                        .apply(\n                            lambda x: any(\n                                s in x for s in [s.lower() for s in filter_databases]\n                            )\n                        )\n                    )\n                    df_history_filtered = df_history_filtered[mask]\n\n                if filter_schemas is not None:\n                    mask = (\n                        df_history_filtered[\"QUERY_TEXT\"]\n                        .str.lower()\n                        .apply(\n                            lambda x: any(\n                                s in x for s in [s.lower() for s in filter_schemas]\n                            )\n                        )\n                    )\n                    df_history_filtered = df_history_filtered[mask]\n\n                if len(df_history_filtered) > 10:\n                    df_history_filtered = df_history_filtered.sample(10)\n\n                for query in df_history_filtered[\"QUERY_TEXT\"].unique().tolist():\n                    plan._plan.append(\n                        TrainingPlanItem(\n                            item_type=TrainingPlanItem.ITEM_TYPE_SQL,\n                            item_group=\"\",\n                            item_name=self.generate_question(query),\n                            item_value=query,\n                        )\n                    )\n\n            except Exception as e:\n                print(e)\n\n        databases = self._get_databases()\n\n        for database in databases:\n            if filter_databases is not None and database not in filter_databases:\n                continue\n\n            try:\n                df_tables = self._get_information_schema_tables(database=database)\n\n                print(f\"Trying INFORMATION_SCHEMA.COLUMNS for {database}\")\n                df_columns = self.run_sql(\n                    f\"SELECT * FROM {database}.INFORMATION_SCHEMA.COLUMNS\"\n                )\n\n                for schema in df_tables[\"TABLE_SCHEMA\"].unique().tolist():\n                    if filter_schemas is not None and schema not in filter_schemas:\n                        continue\n\n                    if (\n                        not include_information_schema\n                        and schema == \"INFORMATION_SCHEMA\"\n                    ):\n                        continue\n\n                    df_columns_filtered_to_schema = df_columns.query(\n                        f\"TABLE_SCHEMA == '{schema}'\"\n                    )\n\n                    try:\n                        tables = (\n                            df_columns_filtered_to_schema[\"TABLE_NAME\"]\n                            .unique()\n                            .tolist()\n                        )\n\n                        for table in tables:\n                            df_columns_filtered_to_table = (\n                                df_columns_filtered_to_schema.query(\n                                    f\"TABLE_NAME == '{table}'\"\n                                )\n                            )\n                            doc = f\"The following columns are in the {table} table in the {database} database:\\n\\n\"\n                            doc += df_columns_filtered_to_table[\n                                [\n                                    \"TABLE_CATALOG\",\n                                    \"TABLE_SCHEMA\",\n                                    \"TABLE_NAME\",\n                                    \"COLUMN_NAME\",\n                                    \"DATA_TYPE\",\n                                    \"COMMENT\",\n                                ]\n                            ].to_markdown()\n\n                            plan._plan.append(\n                                TrainingPlanItem(\n                                    item_type=TrainingPlanItem.ITEM_TYPE_IS,\n                                    item_group=f\"{database}.{schema}\",\n                                    item_name=table,\n                                    item_value=doc,\n                                )\n                            )\n\n                    except Exception as e:\n                        print(e)\n                        pass\n            except Exception as e:\n                print(e)\n\n        return plan\n\n    def get_plotly_figure(\n        self, plotly_code: str, df: pd.DataFrame, dark_mode: bool = True\n    ) -> plotly.graph_objs.Figure:\n        \"\"\"\n        **Example:**\n        ```python\n        fig = vn.get_plotly_figure(\n            plotly_code=\"fig = px.bar(df, x='name', y='salary')\",\n            df=df\n        )\n        fig.show()\n        ```\n        Get a Plotly figure from a dataframe and Plotly code.\n\n        Args:\n            df (pd.DataFrame): The dataframe to use.\n            plotly_code (str): The Plotly code to use.\n\n        Returns:\n            plotly.graph_objs.Figure: The Plotly figure.\n        \"\"\"\n        ldict = {\"df\": df, \"px\": px, \"go\": go}\n        try:\n            exec(plotly_code, globals(), ldict)\n\n            fig = ldict.get(\"fig\", None)\n        except Exception as e:\n            # Inspect data types\n            numeric_cols = df.select_dtypes(include=[\"number\"]).columns.tolist()\n            categorical_cols = df.select_dtypes(\n                include=[\"object\", \"category\"]\n            ).columns.tolist()\n\n            # Decision-making for plot type\n            if len(numeric_cols) >= 2:\n                # Use the first two numeric columns for a scatter plot\n                fig = px.scatter(df, x=numeric_cols[0], y=numeric_cols[1])\n            elif len(numeric_cols) == 1 and len(categorical_cols) >= 1:\n                # Use a bar plot if there's one numeric and one categorical column\n                fig = px.bar(df, x=categorical_cols[0], y=numeric_cols[0])\n            elif len(categorical_cols) >= 1 and df[categorical_cols[0]].nunique() < 10:\n                # Use a pie chart for categorical data with fewer unique values\n                fig = px.pie(df, names=categorical_cols[0])\n            else:\n                # Default to a simple line plot if above conditions are not met\n                fig = px.line(df)\n\n        if fig is None:\n            return None\n\n        if dark_mode:\n            fig.update_layout(template=\"plotly_dark\")\n\n        return fig\n"
  },
  {
    "path": "src/vanna/legacy/bedrock/__init__.py",
    "content": "from .bedrock_converse import Bedrock_Converse\n"
  },
  {
    "path": "src/vanna/legacy/bedrock/bedrock_converse.py",
    "content": "from ..base import VannaBase\n\ntry:\n    import boto3\n    from botocore.exceptions import ClientError\nexcept ImportError:\n    raise ImportError(\"Please install boto3 and botocore to use Amazon Bedrock models\")\n\n\nclass Bedrock_Converse(VannaBase):\n    def __init__(self, client=None, config=None):\n        VannaBase.__init__(self, config=config)\n\n        # default parameters\n        self.temperature = 0.0\n        self.max_tokens = 500\n\n        if client is None:\n            raise ValueError(\n                \"A valid Bedrock runtime client must be provided to invoke Bedrock models\"\n            )\n        else:\n            self.client = client\n\n        if config is None:\n            raise ValueError(\n                \"Config is required with model_id and inference parameters\"\n            )\n\n        if \"modelId\" not in config:\n            raise ValueError(\"config must contain a modelId to invoke\")\n        else:\n            self.model = config[\"modelId\"]\n\n        if \"temperature\" in config:\n            self.temperature = config[\"temperature\"]\n\n        if \"max_tokens\" in config:\n            self.max_tokens = config[\"max_tokens\"]\n\n    def system_message(self, message: str) -> dict:\n        return {\"role\": \"system\", \"content\": message}\n\n    def user_message(self, message: str) -> dict:\n        return {\"role\": \"user\", \"content\": message}\n\n    def assistant_message(self, message: str) -> dict:\n        return {\"role\": \"assistant\", \"content\": message}\n\n    def submit_prompt(self, prompt, **kwargs) -> str:\n        inference_config = {\n            \"temperature\": self.temperature,\n            \"maxTokens\": self.max_tokens,\n        }\n        additional_model_fields = {\n            \"top_p\": 1,  # setting top_p value for nucleus sampling\n        }\n\n        system_message = None\n        no_system_prompt = []\n        for prompt_message in prompt:\n            role = prompt_message[\"role\"]\n            if role == \"system\":\n                system_message = prompt_message[\"content\"]\n            else:\n                no_system_prompt.append(\n                    {\"role\": role, \"content\": [{\"text\": prompt_message[\"content\"]}]}\n                )\n\n        converse_api_params = {\n            \"modelId\": self.model,\n            \"messages\": no_system_prompt,\n            \"inferenceConfig\": inference_config,\n            \"additionalModelRequestFields\": additional_model_fields,\n        }\n\n        if system_message:\n            converse_api_params[\"system\"] = [{\"text\": system_message}]\n\n        try:\n            response = self.client.converse(**converse_api_params)\n            text_content = response[\"output\"][\"message\"][\"content\"][0][\"text\"]\n            return text_content\n        except ClientError as err:\n            message = err.response[\"Error\"][\"Message\"]\n            raise Exception(f\"A Bedrock client error occurred: {message}\")\n"
  },
  {
    "path": "src/vanna/legacy/chromadb/__init__.py",
    "content": "from .chromadb_vector import ChromaDB_VectorStore\n"
  },
  {
    "path": "src/vanna/legacy/chromadb/chromadb_vector.py",
    "content": "import json\nfrom typing import List\n\nimport chromadb\nimport pandas as pd\nfrom chromadb.config import Settings\nfrom chromadb.utils import embedding_functions\n\nfrom ..base import VannaBase\nfrom ..utils import deterministic_uuid\n\ndefault_ef = embedding_functions.DefaultEmbeddingFunction()\n\n\nclass ChromaDB_VectorStore(VannaBase):\n    def __init__(self, config=None):\n        VannaBase.__init__(self, config=config)\n        if config is None:\n            config = {}\n\n        path = config.get(\"path\", \".\")\n        self.embedding_function = config.get(\"embedding_function\", default_ef)\n        curr_client = config.get(\"client\", \"persistent\")\n        collection_metadata = config.get(\"collection_metadata\", None)\n        self.n_results_sql = config.get(\"n_results_sql\", config.get(\"n_results\", 10))\n        self.n_results_documentation = config.get(\n            \"n_results_documentation\", config.get(\"n_results\", 10)\n        )\n        self.n_results_ddl = config.get(\"n_results_ddl\", config.get(\"n_results\", 10))\n\n        if curr_client == \"persistent\":\n            self.chroma_client = chromadb.PersistentClient(\n                path=path, settings=Settings(anonymized_telemetry=False)\n            )\n        elif curr_client == \"in-memory\":\n            self.chroma_client = chromadb.EphemeralClient(\n                settings=Settings(anonymized_telemetry=False)\n            )\n        elif isinstance(curr_client, chromadb.api.client.Client):\n            # allow providing client directly\n            self.chroma_client = curr_client\n        else:\n            raise ValueError(f\"Unsupported client was set in config: {curr_client}\")\n\n        self.documentation_collection = self.chroma_client.get_or_create_collection(\n            name=\"documentation\",\n            embedding_function=self.embedding_function,\n            metadata=collection_metadata,\n        )\n        self.ddl_collection = self.chroma_client.get_or_create_collection(\n            name=\"ddl\",\n            embedding_function=self.embedding_function,\n            metadata=collection_metadata,\n        )\n        self.sql_collection = self.chroma_client.get_or_create_collection(\n            name=\"sql\",\n            embedding_function=self.embedding_function,\n            metadata=collection_metadata,\n        )\n\n    def generate_embedding(self, data: str, **kwargs) -> List[float]:\n        embedding = self.embedding_function([data])\n        if len(embedding) == 1:\n            return embedding[0]\n        return embedding\n\n    def add_question_sql(self, question: str, sql: str, **kwargs) -> str:\n        question_sql_json = json.dumps(\n            {\n                \"question\": question,\n                \"sql\": sql,\n            },\n            ensure_ascii=False,\n        )\n        id = deterministic_uuid(question_sql_json) + \"-sql\"\n        self.sql_collection.add(\n            documents=question_sql_json,\n            embeddings=self.generate_embedding(question_sql_json),\n            ids=id,\n        )\n\n        return id\n\n    def add_ddl(self, ddl: str, **kwargs) -> str:\n        id = deterministic_uuid(ddl) + \"-ddl\"\n        self.ddl_collection.add(\n            documents=ddl,\n            embeddings=self.generate_embedding(ddl),\n            ids=id,\n        )\n        return id\n\n    def add_documentation(self, documentation: str, **kwargs) -> str:\n        id = deterministic_uuid(documentation) + \"-doc\"\n        self.documentation_collection.add(\n            documents=documentation,\n            embeddings=self.generate_embedding(documentation),\n            ids=id,\n        )\n        return id\n\n    def get_training_data(self, **kwargs) -> pd.DataFrame:\n        sql_data = self.sql_collection.get()\n\n        df = pd.DataFrame()\n\n        if sql_data is not None:\n            # Extract the documents and ids\n            documents = [json.loads(doc) for doc in sql_data[\"documents\"]]\n            ids = sql_data[\"ids\"]\n\n            # Create a DataFrame\n            df_sql = pd.DataFrame(\n                {\n                    \"id\": ids,\n                    \"question\": [doc[\"question\"] for doc in documents],\n                    \"content\": [doc[\"sql\"] for doc in documents],\n                }\n            )\n\n            df_sql[\"training_data_type\"] = \"sql\"\n\n            df = pd.concat([df, df_sql])\n\n        ddl_data = self.ddl_collection.get()\n\n        if ddl_data is not None:\n            # Extract the documents and ids\n            documents = [doc for doc in ddl_data[\"documents\"]]\n            ids = ddl_data[\"ids\"]\n\n            # Create a DataFrame\n            df_ddl = pd.DataFrame(\n                {\n                    \"id\": ids,\n                    \"question\": [None for doc in documents],\n                    \"content\": [doc for doc in documents],\n                }\n            )\n\n            df_ddl[\"training_data_type\"] = \"ddl\"\n\n            df = pd.concat([df, df_ddl])\n\n        doc_data = self.documentation_collection.get()\n\n        if doc_data is not None:\n            # Extract the documents and ids\n            documents = [doc for doc in doc_data[\"documents\"]]\n            ids = doc_data[\"ids\"]\n\n            # Create a DataFrame\n            df_doc = pd.DataFrame(\n                {\n                    \"id\": ids,\n                    \"question\": [None for doc in documents],\n                    \"content\": [doc for doc in documents],\n                }\n            )\n\n            df_doc[\"training_data_type\"] = \"documentation\"\n\n            df = pd.concat([df, df_doc])\n\n        return df\n\n    def remove_training_data(self, id: str, **kwargs) -> bool:\n        if id.endswith(\"-sql\"):\n            self.sql_collection.delete(ids=id)\n            return True\n        elif id.endswith(\"-ddl\"):\n            self.ddl_collection.delete(ids=id)\n            return True\n        elif id.endswith(\"-doc\"):\n            self.documentation_collection.delete(ids=id)\n            return True\n        else:\n            return False\n\n    def remove_collection(self, collection_name: str) -> bool:\n        \"\"\"\n        This function can reset the collection to empty state.\n\n        Args:\n            collection_name (str): sql or ddl or documentation\n\n        Returns:\n            bool: True if collection is deleted, False otherwise\n        \"\"\"\n        if collection_name == \"sql\":\n            self.chroma_client.delete_collection(name=\"sql\")\n            self.sql_collection = self.chroma_client.get_or_create_collection(\n                name=\"sql\", embedding_function=self.embedding_function\n            )\n            return True\n        elif collection_name == \"ddl\":\n            self.chroma_client.delete_collection(name=\"ddl\")\n            self.ddl_collection = self.chroma_client.get_or_create_collection(\n                name=\"ddl\", embedding_function=self.embedding_function\n            )\n            return True\n        elif collection_name == \"documentation\":\n            self.chroma_client.delete_collection(name=\"documentation\")\n            self.documentation_collection = self.chroma_client.get_or_create_collection(\n                name=\"documentation\", embedding_function=self.embedding_function\n            )\n            return True\n        else:\n            return False\n\n    @staticmethod\n    def _extract_documents(query_results) -> list:\n        \"\"\"\n        Static method to extract the documents from the results of a query.\n\n        Args:\n            query_results (pd.DataFrame): The dataframe to use.\n\n        Returns:\n            List[str] or None: The extracted documents, or an empty list or\n            single document if an error occurred.\n        \"\"\"\n        if query_results is None:\n            return []\n\n        if \"documents\" in query_results:\n            documents = query_results[\"documents\"]\n\n            if len(documents) == 1 and isinstance(documents[0], list):\n                try:\n                    documents = [json.loads(doc) for doc in documents[0]]\n                except Exception as e:\n                    return documents[0]\n\n            return documents\n\n    def get_similar_question_sql(self, question: str, **kwargs) -> list:\n        return ChromaDB_VectorStore._extract_documents(\n            self.sql_collection.query(\n                query_texts=[question],\n                n_results=self.n_results_sql,\n            )\n        )\n\n    def get_related_ddl(self, question: str, **kwargs) -> list:\n        return ChromaDB_VectorStore._extract_documents(\n            self.ddl_collection.query(\n                query_texts=[question],\n                n_results=self.n_results_ddl,\n            )\n        )\n\n    def get_related_documentation(self, question: str, **kwargs) -> list:\n        return ChromaDB_VectorStore._extract_documents(\n            self.documentation_collection.query(\n                query_texts=[question],\n                n_results=self.n_results_documentation,\n            )\n        )\n"
  },
  {
    "path": "src/vanna/legacy/cohere/__init__.py",
    "content": "from .cohere_chat import Cohere_Chat\nfrom .cohere_embeddings import Cohere_Embeddings\n"
  },
  {
    "path": "src/vanna/legacy/cohere/cohere_chat.py",
    "content": "import os\n\nfrom openai import OpenAI\n\nfrom ..base import VannaBase\n\n\nclass Cohere_Chat(VannaBase):\n    def __init__(self, client=None, config=None):\n        VannaBase.__init__(self, config=config)\n\n        # default parameters - can be overridden using config\n        self.temperature = 0.2  # Lower temperature for more precise SQL generation\n        self.model = \"command-a-03-2025\"  # Cohere's default model\n\n        if config is not None:\n            if \"temperature\" in config:\n                self.temperature = config[\"temperature\"]\n            if \"model\" in config:\n                self.model = config[\"model\"]\n\n        if client is not None:\n            self.client = client\n            return\n\n        # Check for API key in environment variable\n        api_key = os.getenv(\"COHERE_API_KEY\")\n\n        # Check for API key in config\n        if config is not None and \"api_key\" in config:\n            api_key = config[\"api_key\"]\n\n        # Validate API key\n        if not api_key:\n            raise ValueError(\n                \"Cohere API key is required. Please provide it via config or set the COHERE_API_KEY environment variable.\"\n            )\n\n        # Initialize client with validated API key\n        self.client = OpenAI(\n            base_url=\"https://api.cohere.ai/compatibility/v1\",\n            api_key=api_key,\n        )\n\n    def system_message(self, message: str) -> any:\n        return {\n            \"role\": \"developer\",\n            \"content\": message,\n        }  # Cohere uses 'developer' for system role\n\n    def user_message(self, message: str) -> any:\n        return {\"role\": \"user\", \"content\": message}\n\n    def assistant_message(self, message: str) -> any:\n        return {\"role\": \"assistant\", \"content\": message}\n\n    def submit_prompt(self, prompt, **kwargs) -> str:\n        if prompt is None:\n            raise Exception(\"Prompt is None\")\n\n        if len(prompt) == 0:\n            raise Exception(\"Prompt is empty\")\n\n        # Count the number of tokens in the message log\n        # Use 4 as an approximation for the number of characters per token\n        num_tokens = 0\n        for message in prompt:\n            num_tokens += len(message[\"content\"]) / 4\n\n        # Use model from kwargs, config, or default\n        model = kwargs.get(\"model\", self.model)\n        if self.config is not None and \"model\" in self.config and model == self.model:\n            model = self.config[\"model\"]\n\n        print(f\"Using model {model} for {num_tokens} tokens (approx)\")\n        try:\n            response = self.client.chat.completions.create(\n                model=model,\n                messages=prompt,\n                temperature=self.temperature,\n            )\n\n            # Check if response has expected structure\n            if not response or not hasattr(response, \"choices\") or not response.choices:\n                raise ValueError(\"Received empty or malformed response from API\")\n\n            if not response.choices[0] or not hasattr(response.choices[0], \"message\"):\n                raise ValueError(\"Response is missing expected 'message' field\")\n\n            if not hasattr(response.choices[0].message, \"content\"):\n                raise ValueError(\"Response message is missing expected 'content' field\")\n\n            return response.choices[0].message.content\n\n        except Exception as e:\n            # Log the error and raise a more informative exception\n            error_msg = f\"Error processing Cohere chat response: {str(e)}\"\n            print(error_msg)\n            raise Exception(error_msg)\n"
  },
  {
    "path": "src/vanna/legacy/cohere/cohere_embeddings.py",
    "content": "import os\n\nfrom openai import OpenAI\n\nfrom ..base import VannaBase\n\n\nclass Cohere_Embeddings(VannaBase):\n    def __init__(self, client=None, config=None):\n        VannaBase.__init__(self, config=config)\n\n        # Default embedding model\n        self.model = \"embed-multilingual-v3.0\"\n\n        if config is not None and \"model\" in config:\n            self.model = config[\"model\"]\n\n        if client is not None:\n            self.client = client\n            return\n\n        # Check for API key in environment variable\n        api_key = os.getenv(\"COHERE_API_KEY\")\n\n        # Check for API key in config\n        if config is not None and \"api_key\" in config:\n            api_key = config[\"api_key\"]\n\n        # Validate API key\n        if not api_key:\n            raise ValueError(\n                \"Cohere API key is required. Please provide it via config or set the COHERE_API_KEY environment variable.\"\n            )\n\n        # Initialize client with validated API key\n        self.client = OpenAI(\n            base_url=\"https://api.cohere.ai/compatibility/v1\",\n            api_key=api_key,\n        )\n\n    def generate_embedding(self, data: str, **kwargs) -> list[float]:\n        if not data:\n            raise ValueError(\"Cannot generate embedding for empty input data\")\n\n        # Use model from kwargs, config, or default\n        model = kwargs.get(\"model\", self.model)\n        if self.config is not None and \"model\" in self.config and model == self.model:\n            model = self.config[\"model\"]\n\n        try:\n            embedding = self.client.embeddings.create(\n                model=model,\n                input=data,\n                encoding_format=\"float\",  # Ensure we get float values\n            )\n\n            # Check if response has expected structure\n            if not embedding or not hasattr(embedding, \"data\") or not embedding.data:\n                raise ValueError(\n                    \"Received empty or malformed embedding response from API\"\n                )\n\n            if not embedding.data[0] or not hasattr(embedding.data[0], \"embedding\"):\n                raise ValueError(\n                    \"Embedding response is missing expected 'embedding' field\"\n                )\n\n            if not embedding.data[0].embedding:\n                raise ValueError(\"Received empty embedding vector\")\n\n            return embedding.data[0].embedding\n\n        except Exception as e:\n            # Log the error and raise a more informative exception\n            error_msg = f\"Error generating embedding with Cohere: {str(e)}\"\n            print(error_msg)\n            raise Exception(error_msg)\n"
  },
  {
    "path": "src/vanna/legacy/deepseek/__init__.py",
    "content": "from .deepseek_chat import DeepSeekChat\n"
  },
  {
    "path": "src/vanna/legacy/deepseek/deepseek_chat.py",
    "content": "import os\n\nfrom openai import OpenAI\n\nfrom ..base import VannaBase\n\n\n# from vanna.chromadb import ChromaDB_VectorStore\n\n# class DeepSeekVanna(ChromaDB_VectorStore, DeepSeekChat):\n#     def __init__(self, config=None):\n#         ChromaDB_VectorStore.__init__(self, config=config)\n#         DeepSeekChat.__init__(self, config=config)\n\n# vn = DeepSeekVanna(config={\"api_key\": \"sk-************\", \"model\": \"deepseek-chat\"})\n\n\nclass DeepSeekChat(VannaBase):\n    def __init__(self, config=None):\n        if config is None:\n            raise ValueError(\n                \"For DeepSeek, config must be provided with an api_key and model\"\n            )\n        if \"api_key\" not in config:\n            raise ValueError(\"config must contain a DeepSeek api_key\")\n\n        if \"model\" not in config:\n            raise ValueError(\"config must contain a DeepSeek model\")\n\n        api_key = config[\"api_key\"]\n        model = config[\"model\"]\n        self.model = model\n        self.client = OpenAI(api_key=api_key, base_url=\"https://api.deepseek.com/v1\")\n\n    def system_message(self, message: str) -> any:\n        return {\"role\": \"system\", \"content\": message}\n\n    def user_message(self, message: str) -> any:\n        return {\"role\": \"user\", \"content\": message}\n\n    def assistant_message(self, message: str) -> any:\n        return {\"role\": \"assistant\", \"content\": message}\n\n    def generate_sql(self, question: str, **kwargs) -> str:\n        # 使用父类的 generate_sql\n        sql = super().generate_sql(question, **kwargs)\n\n        # 替换 \"\\_\" 为 \"_\"\n        sql = sql.replace(\"\\\\_\", \"_\")\n\n        return sql\n\n    def submit_prompt(self, prompt, **kwargs) -> str:\n        chat_response = self.client.chat.completions.create(\n            model=self.model,\n            messages=prompt,\n        )\n\n        return chat_response.choices[0].message.content\n"
  },
  {
    "path": "src/vanna/legacy/exceptions/__init__.py",
    "content": "class ImproperlyConfigured(Exception):\n    \"\"\"Raise for incorrect configuration.\"\"\"\n\n    pass\n\n\nclass DependencyError(Exception):\n    \"\"\"Raise for missing dependencies.\"\"\"\n\n    pass\n\n\nclass ConnectionError(Exception):\n    \"\"\"Raise for connection\"\"\"\n\n    pass\n\n\nclass OTPCodeError(Exception):\n    \"\"\"Raise for invalid otp or not able to send it\"\"\"\n\n    pass\n\n\nclass SQLRemoveError(Exception):\n    \"\"\"Raise when not able to remove SQL\"\"\"\n\n    pass\n\n\nclass ExecutionError(Exception):\n    \"\"\"Raise when not able to execute Code\"\"\"\n\n    pass\n\n\nclass ValidationError(Exception):\n    \"\"\"Raise for validations\"\"\"\n\n    pass\n\n\nclass APIError(Exception):\n    \"\"\"Raise for API errors\"\"\"\n\n    pass\n"
  },
  {
    "path": "src/vanna/legacy/faiss/__init__.py",
    "content": "from .faiss import FAISS\n"
  },
  {
    "path": "src/vanna/legacy/faiss/faiss.py",
    "content": "import os\nimport json\nimport uuid\nfrom typing import List, Dict, Any\n\nimport faiss\nimport numpy as np\nimport pandas as pd\n\nfrom ..base import VannaBase\nfrom ..exceptions import DependencyError\n\n\nclass FAISS(VannaBase):\n    def __init__(self, config=None):\n        if config is None:\n            config = {}\n\n        VannaBase.__init__(self, config=config)\n\n        try:\n            import faiss\n        except ImportError:\n            raise DependencyError(\n                \"FAISS is not installed. Please install it with 'pip install faiss-cpu' or 'pip install faiss-gpu'\"\n            )\n\n        try:\n            from sentence_transformers import SentenceTransformer\n        except ImportError:\n            raise DependencyError(\n                \"SentenceTransformer is not installed. Please install it with 'pip install sentence-transformers'.\"\n            )\n\n        self.path = config.get(\"path\", \".\")\n        self.embedding_dim = config.get(\"embedding_dim\", 384)\n        self.n_results_sql = config.get(\"n_results_sql\", config.get(\"n_results\", 10))\n        self.n_results_ddl = config.get(\"n_results_ddl\", config.get(\"n_results\", 10))\n        self.n_results_documentation = config.get(\n            \"n_results_documentation\", config.get(\"n_results\", 10)\n        )\n        self.curr_client = config.get(\"client\", \"persistent\")\n\n        if self.curr_client == \"persistent\":\n            self.sql_index = self._load_or_create_index(\"sql_index.faiss\")\n            self.ddl_index = self._load_or_create_index(\"ddl_index.faiss\")\n            self.doc_index = self._load_or_create_index(\"doc_index.faiss\")\n        elif self.curr_client == \"in-memory\":\n            self.sql_index = faiss.IndexFlatL2(self.embedding_dim)\n            self.ddl_index = faiss.IndexFlatL2(self.embedding_dim)\n            self.doc_index = faiss.IndexFlatL2(self.embedding_dim)\n        elif (\n            isinstance(self.curr_client, list)\n            and len(self.curr_client) == 3\n            and all(isinstance(idx, faiss.Index) for idx in self.curr_client)\n        ):\n            self.sql_index = self.curr_client[0]\n            self.ddl_index = self.curr_client[1]\n            self.doc_index = self.curr_client[2]\n        else:\n            raise ValueError(\n                f\"Unsupported storage type was set in config: {self.curr_client}\"\n            )\n\n        self.sql_metadata: List[Dict[str, Any]] = self._load_or_create_metadata(\n            \"sql_metadata.json\"\n        )\n        self.ddl_metadata: List[Dict[str, str]] = self._load_or_create_metadata(\n            \"ddl_metadata.json\"\n        )\n        self.doc_metadata: List[Dict[str, str]] = self._load_or_create_metadata(\n            \"doc_metadata.json\"\n        )\n\n        model_name = config.get(\"embedding_model\", \"all-MiniLM-L6-v2\")\n        self.embedding_model = SentenceTransformer(model_name)\n\n    def _load_or_create_index(self, filename):\n        filepath = os.path.join(self.path, filename)\n        if os.path.exists(filepath):\n            return faiss.read_index(filepath)\n        return faiss.IndexFlatL2(self.embedding_dim)\n\n    def _load_or_create_metadata(self, filename):\n        filepath = os.path.join(self.path, filename)\n        if os.path.exists(filepath):\n            with open(filepath, \"r\") as f:\n                return json.load(f)\n        return []\n\n    def _save_index(self, index, filename):\n        if self.curr_client == \"persistent\":\n            filepath = os.path.join(self.path, filename)\n            faiss.write_index(index, filepath)\n\n    def _save_metadata(self, metadata, filename):\n        if self.curr_client == \"persistent\":\n            filepath = os.path.join(self.path, filename)\n            with open(filepath, \"w\") as f:\n                json.dump(metadata, f)\n\n    def generate_embedding(self, data: str, **kwargs) -> List[float]:\n        embedding = self.embedding_model.encode(data)\n        assert embedding.shape[0] == self.embedding_dim, (\n            f\"Embedding dimension mismatch: expected {self.embedding_dim}, got {embedding.shape[0]}\"\n        )\n        return embedding.tolist()\n\n    def _add_to_index(self, index, metadata_list, text, extra_metadata=None) -> str:\n        embedding = self.generate_embedding(text)\n        index.add(np.array([embedding], dtype=np.float32))\n        entry_id = str(uuid.uuid4())\n        metadata_list.append({\"id\": entry_id, **(extra_metadata or {})})\n        return entry_id\n\n    def add_question_sql(self, question: str, sql: str, **kwargs) -> str:\n        entry_id = self._add_to_index(\n            self.sql_index,\n            self.sql_metadata,\n            question + \" \" + sql,\n            {\"question\": question, \"sql\": sql},\n        )\n        self._save_index(self.sql_index, \"sql_index.faiss\")\n        self._save_metadata(self.sql_metadata, \"sql_metadata.json\")\n        return entry_id\n\n    def add_ddl(self, ddl: str, **kwargs) -> str:\n        entry_id = self._add_to_index(\n            self.ddl_index, self.ddl_metadata, ddl, {\"ddl\": ddl}\n        )\n        self._save_index(self.ddl_index, \"ddl_index.faiss\")\n        self._save_metadata(self.ddl_metadata, \"ddl_metadata.json\")\n        return entry_id\n\n    def add_documentation(self, documentation: str, **kwargs) -> str:\n        entry_id = self._add_to_index(\n            self.doc_index,\n            self.doc_metadata,\n            documentation,\n            {\"documentation\": documentation},\n        )\n        self._save_index(self.doc_index, \"doc_index.faiss\")\n        self._save_metadata(self.doc_metadata, \"doc_metadata.json\")\n        return entry_id\n\n    def _get_similar(self, index, metadata_list, text, n_results) -> list:\n        embedding = self.generate_embedding(text)\n        D, I = index.search(np.array([embedding], dtype=np.float32), k=n_results)\n        return (\n            [] if len(I[0]) == 0 or I[0][0] == -1 else [metadata_list[i] for i in I[0]]\n        )\n\n    def get_similar_question_sql(self, question: str, **kwargs) -> list:\n        return self._get_similar(\n            self.sql_index, self.sql_metadata, question, self.n_results_sql\n        )\n\n    def get_related_ddl(self, question: str, **kwargs) -> list:\n        return [\n            metadata[\"ddl\"]\n            for metadata in self._get_similar(\n                self.ddl_index, self.ddl_metadata, question, self.n_results_ddl\n            )\n        ]\n\n    def get_related_documentation(self, question: str, **kwargs) -> list:\n        return [\n            metadata[\"documentation\"]\n            for metadata in self._get_similar(\n                self.doc_index,\n                self.doc_metadata,\n                question,\n                self.n_results_documentation,\n            )\n        ]\n\n    def get_training_data(self, **kwargs) -> pd.DataFrame:\n        sql_data = pd.DataFrame(self.sql_metadata)\n        sql_data[\"training_data_type\"] = \"sql\"\n\n        ddl_data = pd.DataFrame(self.ddl_metadata)\n        ddl_data[\"training_data_type\"] = \"ddl\"\n\n        doc_data = pd.DataFrame(self.doc_metadata)\n        doc_data[\"training_data_type\"] = \"documentation\"\n\n        return pd.concat([sql_data, ddl_data, doc_data], ignore_index=True)\n\n    def remove_training_data(self, id: str, **kwargs) -> bool:\n        for metadata_list, index, index_name in [\n            (self.sql_metadata, self.sql_index, \"sql_index.faiss\"),\n            (self.ddl_metadata, self.ddl_index, \"ddl_index.faiss\"),\n            (self.doc_metadata, self.doc_index, \"doc_index.faiss\"),\n        ]:\n            for i, item in enumerate(metadata_list):\n                if item[\"id\"] == id:\n                    del metadata_list[i]\n                    new_index = faiss.IndexFlatL2(self.embedding_dim)\n                    embeddings = [\n                        self.generate_embedding(json.dumps(m)) for m in metadata_list\n                    ]\n                    if embeddings:\n                        new_index.add(np.array(embeddings, dtype=np.float32))\n                    setattr(self, index_name.split(\".\")[0], new_index)\n\n                    if self.curr_client == \"persistent\":\n                        self._save_index(new_index, index_name)\n                        self._save_metadata(\n                            metadata_list, f\"{index_name.split('.')[0]}_metadata.json\"\n                        )\n\n                    return True\n        return False\n\n    def remove_collection(self, collection_name: str) -> bool:\n        if collection_name in [\"sql\", \"ddl\", \"documentation\"]:\n            setattr(\n                self, f\"{collection_name}_index\", faiss.IndexFlatL2(self.embedding_dim)\n            )\n            setattr(self, f\"{collection_name}_metadata\", [])\n\n            if self.curr_client == \"persistent\":\n                self._save_index(\n                    getattr(self, f\"{collection_name}_index\"),\n                    f\"{collection_name}_index.faiss\",\n                )\n                self._save_metadata([], f\"{collection_name}_metadata.json\")\n\n            return True\n        return False\n"
  },
  {
    "path": "src/vanna/legacy/flask/__init__.py",
    "content": "import json\nimport logging\nimport os\nimport sys\nimport uuid\nfrom abc import ABC, abstractmethod\nfrom functools import wraps\nimport importlib.metadata\n\nimport flask\nimport requests\nfrom flasgger import Swagger\nfrom flask import Flask, Response, jsonify, request, send_from_directory\nfrom flask_sock import Sock\n\nfrom ..base import VannaBase\nfrom .assets import css_content, html_content, js_content\nfrom .auth import AuthInterface, NoAuth\n\n\nclass Cache(ABC):\n    \"\"\"\n    Define the interface for a cache that can be used to store data in a Flask app.\n    \"\"\"\n\n    @abstractmethod\n    def generate_id(self, *args, **kwargs):\n        \"\"\"\n        Generate a unique ID for the cache.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get(self, id, field):\n        \"\"\"\n        Get a value from the cache.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_all(self, field_list) -> list:\n        \"\"\"\n        Get all values from the cache.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def set(self, id, field, value):\n        \"\"\"\n        Set a value in the cache.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def delete(self, id):\n        \"\"\"\n        Delete a value from the cache.\n        \"\"\"\n        pass\n\n\nclass MemoryCache(Cache):\n    def __init__(self):\n        self.cache = {}\n\n    def generate_id(self, *args, **kwargs):\n        return str(uuid.uuid4())\n\n    def set(self, id, field, value):\n        if id not in self.cache:\n            self.cache[id] = {}\n\n        self.cache[id][field] = value\n\n    def get(self, id, field):\n        if id not in self.cache:\n            return None\n\n        if field not in self.cache[id]:\n            return None\n\n        return self.cache[id][field]\n\n    def get_all(self, field_list) -> list:\n        return [\n            {\"id\": id, **{field: self.get(id=id, field=field) for field in field_list}}\n            for id in self.cache\n        ]\n\n    def delete(self, id):\n        if id in self.cache:\n            del self.cache[id]\n\n\nclass VannaFlaskAPI:\n    flask_app = None\n\n    def requires_cache(self, required_fields, optional_fields=[]):\n        def decorator(f):\n            @wraps(f)\n            def decorated(*args, **kwargs):\n                id = request.args.get(\"id\")\n\n                if id is None:\n                    id = request.json.get(\"id\")\n                    if id is None:\n                        return jsonify({\"type\": \"error\", \"error\": \"No id provided\"})\n\n                for field in required_fields:\n                    if self.cache.get(id=id, field=field) is None:\n                        return jsonify({\"type\": \"error\", \"error\": f\"No {field} found\"})\n\n                field_values = {\n                    field: self.cache.get(id=id, field=field)\n                    for field in required_fields\n                }\n\n                for field in optional_fields:\n                    field_values[field] = self.cache.get(id=id, field=field)\n\n                # Add the id to the field_values\n                field_values[\"id\"] = id\n\n                return f(*args, **field_values, **kwargs)\n\n            return decorated\n\n        return decorator\n\n    def requires_auth(self, f):\n        @wraps(f)\n        def decorated(*args, **kwargs):\n            user = self.auth.get_user(flask.request)\n\n            if not self.auth.is_logged_in(user):\n                return jsonify(\n                    {\"type\": \"not_logged_in\", \"html\": self.auth.login_form()}\n                )\n\n            # Pass the user to the function\n            return f(*args, user=user, **kwargs)\n\n        return decorated\n\n    def __init__(\n        self,\n        vn: VannaBase,\n        cache: Cache = MemoryCache(),\n        auth: AuthInterface = NoAuth(),\n        debug=True,\n        allow_llm_to_see_data=False,\n        chart=True,\n    ):\n        \"\"\"\n        Expose a Flask API that can be used to interact with a Vanna instance.\n\n        Args:\n            vn: The Vanna instance to interact with.\n            cache: The cache to use. Defaults to MemoryCache, which uses an in-memory cache. You can also pass in a custom cache that implements the Cache interface.\n            auth: The authentication method to use. Defaults to NoAuth, which doesn't require authentication. You can also pass in a custom authentication method that implements the AuthInterface interface.\n            debug: Show the debug console. Defaults to True.\n            allow_llm_to_see_data: Whether to allow the LLM to see data. Defaults to False.\n            chart: Whether to show the chart output in the UI. Defaults to True.\n\n        Returns:\n            None\n        \"\"\"\n\n        self.flask_app = Flask(__name__)\n\n        self.swagger = Swagger(\n            self.flask_app, template={\"info\": {\"title\": \"Vanna API\"}}\n        )\n        self.sock = Sock(self.flask_app)\n        self.ws_clients = []\n        self.vn = vn\n        self.auth = auth\n        self.cache = cache\n        self.debug = debug\n        self.allow_llm_to_see_data = allow_llm_to_see_data\n        self.chart = chart\n        self.config = {\n            \"debug\": debug,\n            \"allow_llm_to_see_data\": allow_llm_to_see_data,\n            \"chart\": chart,\n        }\n        log = logging.getLogger(\"werkzeug\")\n        log.setLevel(logging.ERROR)\n\n        if \"google.colab\" in sys.modules:\n            self.debug = False\n            print(\n                \"Google Colab doesn't support running websocket servers. Disabling debug mode.\"\n            )\n\n        if self.debug:\n\n            def log(message, title=\"Info\"):\n                [\n                    ws.send(json.dumps({\"message\": message, \"title\": title}))\n                    for ws in self.ws_clients\n                ]\n\n            self.vn.log = log\n\n        @self.flask_app.route(\"/api/v0/get_config\", methods=[\"GET\"])\n        @self.requires_auth\n        def get_config(user: any):\n            \"\"\"\n            Get the configuration for a user\n            ---\n            parameters:\n              - name: user\n                in: query\n            responses:\n              200:\n                schema:\n                  type: object\n                  properties:\n                    type:\n                      type: string\n                      default: config\n                    config:\n                      type: object\n            \"\"\"\n            config = self.auth.override_config_for_user(user, self.config)\n            return jsonify({\"type\": \"config\", \"config\": config})\n\n        @self.flask_app.route(\"/api/v0/generate_questions\", methods=[\"GET\"])\n        @self.requires_auth\n        def generate_questions(user: any):\n            \"\"\"\n            Generate questions\n            ---\n            parameters:\n              - name: user\n                in: query\n            responses:\n              200:\n                schema:\n                  type: object\n                  properties:\n                    type:\n                      type: string\n                      default: question_list\n                    questions:\n                      type: array\n                      items:\n                        type: string\n                    header:\n                      type: string\n                      default: Here are some questions you can ask\n            \"\"\"\n            # If self has an _model attribute and model=='chinook'\n            if hasattr(self.vn, \"_model\") and self.vn._model == \"chinook\":\n                return jsonify(\n                    {\n                        \"type\": \"question_list\",\n                        \"questions\": [\n                            \"What are the top 10 artists by sales?\",\n                            \"What are the total sales per year by country?\",\n                            \"Who is the top selling artist in each genre? Show the sales numbers.\",\n                            \"How do the employees rank in terms of sales performance?\",\n                            \"Which 5 cities have the most customers?\",\n                        ],\n                        \"header\": \"Here are some questions you can ask:\",\n                    }\n                )\n\n            training_data = vn.get_training_data()\n\n            # If training data is None or empty\n            if training_data is None or len(training_data) == 0:\n                return jsonify(\n                    {\n                        \"type\": \"error\",\n                        \"error\": \"No training data found. Please add some training data first.\",\n                    }\n                )\n\n            # Get the questions from the training data\n            try:\n                # Filter training data to only include questions where the question is not null\n                questions = (\n                    training_data[training_data[\"question\"].notnull()]\n                    .sample(5)[\"question\"]\n                    .tolist()\n                )\n\n                # Temporarily this will just return an empty list\n                return jsonify(\n                    {\n                        \"type\": \"question_list\",\n                        \"questions\": questions,\n                        \"header\": \"Here are some questions you can ask\",\n                    }\n                )\n            except Exception as e:\n                return jsonify(\n                    {\n                        \"type\": \"question_list\",\n                        \"questions\": [],\n                        \"header\": \"Go ahead and ask a question\",\n                    }\n                )\n\n        @self.flask_app.route(\"/api/v0/generate_sql\", methods=[\"GET\"])\n        @self.requires_auth\n        def generate_sql(user: any):\n            \"\"\"\n            Generate SQL from a question\n            ---\n            parameters:\n              - name: user\n                in: query\n              - name: question\n                in: query\n                type: string\n                required: true\n            responses:\n              200:\n                schema:\n                  type: object\n                  properties:\n                    type:\n                      type: string\n                      default: sql\n                    id:\n                      type: string\n                    text:\n                      type: string\n            \"\"\"\n            question = flask.request.args.get(\"question\")\n\n            if question is None:\n                return jsonify({\"type\": \"error\", \"error\": \"No question provided\"})\n\n            id = self.cache.generate_id(question=question)\n            sql = vn.generate_sql(\n                question=question, allow_llm_to_see_data=self.allow_llm_to_see_data\n            )\n\n            self.cache.set(id=id, field=\"question\", value=question)\n            self.cache.set(id=id, field=\"sql\", value=sql)\n\n            if vn.is_sql_valid(sql=sql):\n                return jsonify(\n                    {\n                        \"type\": \"sql\",\n                        \"id\": id,\n                        \"text\": sql,\n                    }\n                )\n            else:\n                return jsonify(\n                    {\n                        \"type\": \"text\",\n                        \"id\": id,\n                        \"text\": sql,\n                    }\n                )\n\n        @self.flask_app.route(\"/api/v0/generate_rewritten_question\", methods=[\"GET\"])\n        @self.requires_auth\n        def generate_rewritten_question(user: any):\n            \"\"\"\n            Generate a rewritten question\n            ---\n            parameters:\n              - name: last_question\n                in: query\n                type: string\n                required: true\n              - name: new_question\n                in: query\n                type: string\n                required: true\n            \"\"\"\n\n            last_question = flask.request.args.get(\"last_question\")\n            new_question = flask.request.args.get(\"new_question\")\n\n            rewritten_question = self.vn.generate_rewritten_question(\n                last_question, new_question\n            )\n\n            return jsonify(\n                {\"type\": \"rewritten_question\", \"question\": rewritten_question}\n            )\n\n        @self.flask_app.route(\"/api/v0/get_function\", methods=[\"GET\"])\n        @self.requires_auth\n        def get_function(user: any):\n            \"\"\"\n            Get a function from a question\n            ---\n            parameters:\n              - name: user\n                in: query\n              - name: question\n                in: query\n                type: string\n                required: true\n            responses:\n              200:\n                schema:\n                  type: object\n                  properties:\n                    type:\n                      type: string\n                      default: function\n                    id:\n                      type: object\n                    function:\n                      type: string\n            \"\"\"\n            question = flask.request.args.get(\"question\")\n\n            if question is None:\n                return jsonify({\"type\": \"error\", \"error\": \"No question provided\"})\n\n            if not hasattr(vn, \"get_function\"):\n                return jsonify(\n                    {\n                        \"type\": \"error\",\n                        \"error\": \"This setup does not support function generation.\",\n                    }\n                )\n\n            id = self.cache.generate_id(question=question)\n            function = vn.get_function(question=question)\n\n            if function is None:\n                return jsonify({\"type\": \"error\", \"error\": \"No function found\"})\n\n            if \"instantiated_sql\" not in function:\n                self.vn.log(f\"No instantiated SQL found for {question} in {function}\")\n                return jsonify({\"type\": \"error\", \"error\": \"No instantiated SQL found\"})\n\n            self.cache.set(id=id, field=\"question\", value=question)\n            self.cache.set(id=id, field=\"sql\", value=function[\"instantiated_sql\"])\n\n            if (\n                \"instantiated_post_processing_code\" in function\n                and function[\"instantiated_post_processing_code\"] is not None\n                and len(function[\"instantiated_post_processing_code\"]) > 0\n            ):\n                self.cache.set(\n                    id=id,\n                    field=\"plotly_code\",\n                    value=function[\"instantiated_post_processing_code\"],\n                )\n\n            return jsonify(\n                {\n                    \"type\": \"function\",\n                    \"id\": id,\n                    \"function\": function,\n                }\n            )\n\n        @self.flask_app.route(\"/api/v0/get_all_functions\", methods=[\"GET\"])\n        @self.requires_auth\n        def get_all_functions(user: any):\n            \"\"\"\n            Get all the functions\n            ---\n            parameters:\n              - name: user\n                in: query\n            responses:\n              200:\n                schema:\n                  type: object\n                  properties:\n                    type:\n                      type: string\n                      default: functions\n                    functions:\n                      type: array\n            \"\"\"\n            if not hasattr(vn, \"get_all_functions\"):\n                return jsonify(\n                    {\n                        \"type\": \"error\",\n                        \"error\": \"This setup does not support function generation.\",\n                    }\n                )\n\n            functions = vn.get_all_functions()\n\n            return jsonify(\n                {\n                    \"type\": \"functions\",\n                    \"functions\": functions,\n                }\n            )\n\n        @self.flask_app.route(\"/api/v0/run_sql\", methods=[\"GET\"])\n        @self.requires_auth\n        @self.requires_cache([\"sql\"])\n        def run_sql(user: any, id: str, sql: str):\n            \"\"\"\n            Run SQL\n            ---\n            parameters:\n              - name: user\n                in: query\n              - name: id\n                in: query|body\n                type: string\n                required: true\n            responses:\n              200:\n                schema:\n                  type: object\n                  properties:\n                    type:\n                      type: string\n                      default: df\n                    id:\n                      type: string\n                    df:\n                      type: object\n                    should_generate_chart:\n                      type: boolean\n            \"\"\"\n            try:\n                if not vn.run_sql_is_set:\n                    return jsonify(\n                        {\n                            \"type\": \"error\",\n                            \"error\": \"Please connect to a database using vn.connect_to_... in order to run SQL queries.\",\n                        }\n                    )\n\n                df = vn.run_sql(sql=sql)\n\n                self.cache.set(id=id, field=\"df\", value=df)\n\n                return jsonify(\n                    {\n                        \"type\": \"df\",\n                        \"id\": id,\n                        \"df\": df.head(10).to_json(orient=\"records\", date_format=\"iso\"),\n                        \"should_generate_chart\": self.chart\n                        and vn.should_generate_chart(df),\n                    }\n                )\n\n            except Exception as e:\n                return jsonify({\"type\": \"sql_error\", \"error\": str(e)})\n\n        @self.flask_app.route(\"/api/v0/fix_sql\", methods=[\"POST\"])\n        @self.requires_auth\n        @self.requires_cache([\"question\", \"sql\"])\n        def fix_sql(user: any, id: str, question: str, sql: str):\n            \"\"\"\n            Fix SQL\n            ---\n            parameters:\n              - name: user\n                in: query\n              - name: id\n                in: query|body\n                type: string\n                required: true\n              - name: error\n                in: body\n                type: string\n                required: true\n            responses:\n              200:\n                schema:\n                  type: object\n                  properties:\n                    type:\n                      type: string\n                      default: sql\n                    id:\n                      type: string\n                    text:\n                      type: string\n            \"\"\"\n            error = flask.request.json.get(\"error\")\n\n            if error is None:\n                return jsonify({\"type\": \"error\", \"error\": \"No error provided\"})\n\n            question = f\"I have an error: {error}\\n\\nHere is the SQL I tried to run: {sql}\\n\\nThis is the question I was trying to answer: {question}\\n\\nCan you rewrite the SQL to fix the error?\"\n\n            fixed_sql = vn.generate_sql(question=question)\n\n            self.cache.set(id=id, field=\"sql\", value=fixed_sql)\n\n            return jsonify(\n                {\n                    \"type\": \"sql\",\n                    \"id\": id,\n                    \"text\": fixed_sql,\n                }\n            )\n\n        @self.flask_app.route(\"/api/v0/update_sql\", methods=[\"POST\"])\n        @self.requires_auth\n        @self.requires_cache([])\n        def update_sql(user: any, id: str):\n            \"\"\"\n            Update SQL\n            ---\n            parameters:\n              - name: user\n                in: query\n              - name: id\n                in: query|body\n                type: string\n                required: true\n              - name: sql\n                in: body\n                type: string\n                required: true\n            responses:\n              200:\n                schema:\n                  type: object\n                  properties:\n                    type:\n                      type: string\n                      default: sql\n                    id:\n                      type: string\n                    text:\n                      type: string\n            \"\"\"\n            sql = flask.request.json.get(\"sql\")\n\n            if sql is None:\n                return jsonify({\"type\": \"error\", \"error\": \"No sql provided\"})\n\n            self.cache.set(id=id, field=\"sql\", value=sql)\n\n            return jsonify(\n                {\n                    \"type\": \"sql\",\n                    \"id\": id,\n                    \"text\": sql,\n                }\n            )\n\n        @self.flask_app.route(\"/api/v0/download_csv\", methods=[\"GET\"])\n        @self.requires_auth\n        @self.requires_cache([\"df\"])\n        def download_csv(user: any, id: str, df):\n            \"\"\"\n            Download CSV\n            ---\n            parameters:\n              - name: user\n                in: query\n              - name: id\n                in: query|body\n                type: string\n                required: true\n            responses:\n              200:\n                description: download CSV\n            \"\"\"\n            csv = df.to_csv()\n\n            return Response(\n                csv,\n                mimetype=\"text/csv\",\n                headers={\"Content-disposition\": f\"attachment; filename={id}.csv\"},\n            )\n\n        @self.flask_app.route(\"/api/v0/generate_plotly_figure\", methods=[\"GET\"])\n        @self.requires_auth\n        @self.requires_cache([\"df\", \"question\", \"sql\"])\n        def generate_plotly_figure(user: any, id: str, df, question, sql):\n            \"\"\"\n            Generate plotly figure\n            ---\n            parameters:\n              - name: user\n                in: query\n              - name: id\n                in: query|body\n                type: string\n                required: true\n              - name: chart_instructions\n                in: body\n                type: string\n            responses:\n              200:\n                schema:\n                  type: object\n                  properties:\n                    type:\n                      type: string\n                      default: plotly_figure\n                    id:\n                      type: string\n                    fig:\n                      type: object\n            \"\"\"\n            chart_instructions = flask.request.args.get(\"chart_instructions\")\n\n            try:\n                # If chart_instructions is not set then attempt to retrieve the code from the cache\n                if chart_instructions is None or len(chart_instructions) == 0:\n                    code = self.cache.get(id=id, field=\"plotly_code\")\n                else:\n                    question = f\"{question}. When generating the chart, use these special instructions: {chart_instructions}\"\n                    code = vn.generate_plotly_code(\n                        question=question,\n                        sql=sql,\n                        df_metadata=f\"Running df.dtypes gives:\\n {df.dtypes}\",\n                    )\n                    self.cache.set(id=id, field=\"plotly_code\", value=code)\n\n                fig = vn.get_plotly_figure(plotly_code=code, df=df, dark_mode=False)\n                fig_json = fig.to_json()\n\n                self.cache.set(id=id, field=\"fig_json\", value=fig_json)\n\n                return jsonify(\n                    {\n                        \"type\": \"plotly_figure\",\n                        \"id\": id,\n                        \"fig\": fig_json,\n                    }\n                )\n            except Exception as e:\n                # Print the stack trace\n                import traceback\n\n                traceback.print_stack()\n                traceback.print_exc()\n\n                return jsonify({\"type\": \"error\", \"error\": str(e)})\n\n        @self.flask_app.route(\"/api/v0/get_training_data\", methods=[\"GET\"])\n        @self.requires_auth\n        def get_training_data(user: any):\n            \"\"\"\n            Get all training data\n            ---\n            parameters:\n              - name: user\n                in: query\n            responses:\n              200:\n                schema:\n                  type: object\n                  properties:\n                    type:\n                      type: string\n                      default: df\n                    id:\n                      type: string\n                      default: training_data\n                    df:\n                      type: object\n            \"\"\"\n            df = vn.get_training_data()\n\n            if df is None or len(df) == 0:\n                return jsonify(\n                    {\n                        \"type\": \"error\",\n                        \"error\": \"No training data found. Please add some training data first.\",\n                    }\n                )\n\n            return jsonify(\n                {\n                    \"type\": \"df\",\n                    \"id\": \"training_data\",\n                    \"df\": df.to_json(orient=\"records\"),\n                }\n            )\n\n        @self.flask_app.route(\"/api/v0/remove_training_data\", methods=[\"POST\"])\n        @self.requires_auth\n        def remove_training_data(user: any):\n            \"\"\"\n            Remove training data\n            ---\n            parameters:\n              - name: user\n                in: query\n              - name: id\n                in: body\n                type: string\n                required: true\n            responses:\n              200:\n                schema:\n                  type: object\n                  properties:\n                    success:\n                      type: boolean\n            \"\"\"\n            # Get id from the JSON body\n            id = flask.request.json.get(\"id\")\n\n            if id is None:\n                return jsonify({\"type\": \"error\", \"error\": \"No id provided\"})\n\n            if vn.remove_training_data(id=id):\n                return jsonify({\"success\": True})\n            else:\n                return jsonify(\n                    {\"type\": \"error\", \"error\": \"Couldn't remove training data\"}\n                )\n\n        @self.flask_app.route(\"/api/v0/train\", methods=[\"POST\"])\n        @self.requires_auth\n        def add_training_data(user: any):\n            \"\"\"\n            Add training data\n            ---\n            parameters:\n              - name: user\n                in: query\n              - name: question\n                in: body\n                type: string\n              - name: sql\n                in: body\n                type: string\n              - name: ddl\n                in: body\n                type: string\n              - name: documentation\n                in: body\n                type: string\n            responses:\n              200:\n                schema:\n                  type: object\n                  properties:\n                    id:\n                      type: string\n            \"\"\"\n            question = flask.request.json.get(\"question\")\n            sql = flask.request.json.get(\"sql\")\n            ddl = flask.request.json.get(\"ddl\")\n            documentation = flask.request.json.get(\"documentation\")\n\n            try:\n                id = vn.train(\n                    question=question, sql=sql, ddl=ddl, documentation=documentation\n                )\n\n                return jsonify({\"id\": id})\n            except Exception as e:\n                print(\"TRAINING ERROR\", e)\n                return jsonify({\"type\": \"error\", \"error\": str(e)})\n\n        @self.flask_app.route(\"/api/v0/create_function\", methods=[\"GET\"])\n        @self.requires_auth\n        @self.requires_cache([\"question\", \"sql\"])\n        def create_function(user: any, id: str, question: str, sql: str):\n            \"\"\"\n            Create function\n            ---\n            parameters:\n              - name: user\n                in: query\n              - name: id\n                in: query|body\n                type: string\n                required: true\n            responses:\n              200:\n                schema:\n                  type: object\n                  properties:\n                    type:\n                      type: string\n                      default: function_template\n                    id:\n                      type: string\n                    function_template:\n                      type: object\n            \"\"\"\n            plotly_code = self.cache.get(id=id, field=\"plotly_code\")\n\n            if plotly_code is None:\n                plotly_code = \"\"\n\n            function_data = self.vn.create_function(\n                question=question, sql=sql, plotly_code=plotly_code\n            )\n\n            return jsonify(\n                {\n                    \"type\": \"function_template\",\n                    \"id\": id,\n                    \"function_template\": function_data,\n                }\n            )\n\n        @self.flask_app.route(\"/api/v0/update_function\", methods=[\"POST\"])\n        @self.requires_auth\n        def update_function(user: any):\n            \"\"\"\n            Update function\n            ---\n            parameters:\n              - name: user\n                in: query\n              - name: old_function_name\n                in: body\n                type: string\n                required: true\n              - name: updated_function\n                in: body\n                type: object\n                required: true\n            responses:\n              200:\n                schema:\n                  type: object\n                  properties:\n                    success:\n                      type: boolean\n            \"\"\"\n            old_function_name = flask.request.json.get(\"old_function_name\")\n            updated_function = flask.request.json.get(\"updated_function\")\n\n            print(\"old_function_name\", old_function_name)\n            print(\"updated_function\", updated_function)\n\n            updated = vn.update_function(\n                old_function_name=old_function_name, updated_function=updated_function\n            )\n\n            return jsonify({\"success\": updated})\n\n        @self.flask_app.route(\"/api/v0/delete_function\", methods=[\"POST\"])\n        @self.requires_auth\n        def delete_function(user: any):\n            \"\"\"\n            Delete function\n            ---\n            parameters:\n              - name: user\n                in: query\n              - name: function_name\n                in: body\n                type: string\n                required: true\n            responses:\n              200:\n                schema:\n                  type: object\n                  properties:\n                    success:\n                      type: boolean\n            \"\"\"\n            function_name = flask.request.json.get(\"function_name\")\n\n            return jsonify({\"success\": vn.delete_function(function_name=function_name)})\n\n        @self.flask_app.route(\"/api/v0/generate_followup_questions\", methods=[\"GET\"])\n        @self.requires_auth\n        @self.requires_cache([\"df\", \"question\", \"sql\"])\n        def generate_followup_questions(user: any, id: str, df, question, sql):\n            \"\"\"\n            Generate followup questions\n            ---\n            parameters:\n              - name: user\n                in: query\n              - name: id\n                in: query|body\n                type: string\n                required: true\n            responses:\n              200:\n                schema:\n                  type: object\n                  properties:\n                    type:\n                      type: string\n                      default: question_list\n                    questions:\n                      type: array\n                      items:\n                        type: string\n                    header:\n                      type: string\n            \"\"\"\n            if self.allow_llm_to_see_data:\n                followup_questions = vn.generate_followup_questions(\n                    question=question, sql=sql, df=df\n                )\n                if followup_questions is not None and len(followup_questions) > 5:\n                    followup_questions = followup_questions[:5]\n\n                self.cache.set(\n                    id=id, field=\"followup_questions\", value=followup_questions\n                )\n\n                return jsonify(\n                    {\n                        \"type\": \"question_list\",\n                        \"id\": id,\n                        \"questions\": followup_questions,\n                        \"header\": \"Here are some potential followup questions:\",\n                    }\n                )\n            else:\n                self.cache.set(id=id, field=\"followup_questions\", value=[])\n                return jsonify(\n                    {\n                        \"type\": \"question_list\",\n                        \"id\": id,\n                        \"questions\": [],\n                        \"header\": \"Followup Questions can be enabled if you set allow_llm_to_see_data=True\",\n                    }\n                )\n\n        @self.flask_app.route(\"/api/v0/generate_summary\", methods=[\"GET\"])\n        @self.requires_auth\n        @self.requires_cache([\"df\", \"question\"])\n        def generate_summary(user: any, id: str, df, question):\n            \"\"\"\n            Generate summary\n            ---\n            parameters:\n              - name: user\n                in: query\n              - name: id\n                in: query|body\n                type: string\n                required: true\n            responses:\n              200:\n                schema:\n                  type: object\n                  properties:\n                    type:\n                      type: string\n                      default: text\n                    id:\n                      type: string\n                    text:\n                      type: string\n            \"\"\"\n            if self.allow_llm_to_see_data:\n                summary = vn.generate_summary(question=question, df=df)\n\n                self.cache.set(id=id, field=\"summary\", value=summary)\n\n                return jsonify(\n                    {\n                        \"type\": \"text\",\n                        \"id\": id,\n                        \"text\": summary,\n                    }\n                )\n            else:\n                return jsonify(\n                    {\n                        \"type\": \"text\",\n                        \"id\": id,\n                        \"text\": \"Summarization can be enabled if you set allow_llm_to_see_data=True\",\n                    }\n                )\n\n        @self.flask_app.route(\"/api/v0/load_question\", methods=[\"GET\"])\n        @self.requires_auth\n        @self.requires_cache(\n            [\"question\", \"sql\", \"df\"], optional_fields=[\"summary\", \"fig_json\"]\n        )\n        def load_question(user: any, id: str, question, sql, df, fig_json, summary):\n            \"\"\"\n            Load question\n            ---\n            parameters:\n              - name: user\n                in: query\n              - name: id\n                in: query|body\n                type: string\n                required: true\n            responses:\n              200:\n                schema:\n                  type: object\n                  properties:\n                    type:\n                      type: string\n                      default: question_cache\n                    id:\n                      type: string\n                    question:\n                      type: string\n                    sql:\n                      type: string\n                    df:\n                      type: object\n                    fig:\n                      type: object\n                    summary:\n                      type: string\n            \"\"\"\n            try:\n                return jsonify(\n                    {\n                        \"type\": \"question_cache\",\n                        \"id\": id,\n                        \"question\": question,\n                        \"sql\": sql,\n                        \"df\": df.head(10).to_json(orient=\"records\", date_format=\"iso\"),\n                        \"fig\": fig_json,\n                        \"summary\": summary,\n                    }\n                )\n\n            except Exception as e:\n                return jsonify({\"type\": \"error\", \"error\": str(e)})\n\n        @self.flask_app.route(\"/api/v0/get_question_history\", methods=[\"GET\"])\n        @self.requires_auth\n        def get_question_history(user: any):\n            \"\"\"\n            Get question history\n            ---\n            parameters:\n              - name: user\n                in: query\n            responses:\n              200:\n                schema:\n                  type: object\n                  properties:\n                    type:\n                      type: string\n                      default: question_history\n                    questions:\n                      type: array\n                      items:\n                        type: string\n            \"\"\"\n            return jsonify(\n                {\n                    \"type\": \"question_history\",\n                    \"questions\": cache.get_all(field_list=[\"question\"]),\n                }\n            )\n\n        @self.flask_app.route(\"/api/v0/<path:catch_all>\", methods=[\"GET\", \"POST\"])\n        def catch_all(catch_all):\n            return jsonify(\n                {\"type\": \"error\", \"error\": \"The rest of the API is not ported yet.\"}\n            )\n\n        if self.debug:\n\n            @self.sock.route(\"/api/v0/log\")\n            def sock_log(ws):\n                self.ws_clients.append(ws)\n\n                try:\n                    while True:\n                        message = (\n                            ws.receive()\n                        )  # This example just reads and ignores to keep the socket open\n                finally:\n                    self.ws_clients.remove(ws)\n\n    def run(self, *args, **kwargs):\n        \"\"\"\n        Run the Flask app.\n\n        Args:\n            *args: Arguments to pass to Flask's run method.\n            **kwargs: Keyword arguments to pass to Flask's run method.\n\n        Returns:\n            None\n        \"\"\"\n        if args or kwargs:\n            self.flask_app.run(*args, **kwargs)\n\n        else:\n            try:\n                from google.colab import output\n\n                output.serve_kernel_port_as_window(8084)\n                from google.colab.output import eval_js\n\n                print(\"Your app is running at:\")\n                print(eval_js(\"google.colab.kernel.proxyPort(8084)\"))\n            except Exception:\n                print(\"Your app is running at:\")\n                print(\"http://localhost:8084\")\n\n            self.flask_app.run(\n                host=\"0.0.0.0\", port=8084, debug=self.debug, use_reloader=False\n            )\n\n\nclass VannaFlaskApp(VannaFlaskAPI):\n    def __init__(\n        self,\n        vn: VannaBase,\n        cache: Cache = MemoryCache(),\n        auth: AuthInterface = NoAuth(),\n        debug=True,\n        allow_llm_to_see_data=False,\n        logo=\"https://img.vanna.ai/vanna-flask.svg\",\n        title=\"Welcome to Vanna.AI\",\n        subtitle=\"Your AI-powered copilot for SQL queries.\",\n        show_training_data=True,\n        suggested_questions=True,\n        sql=True,\n        table=True,\n        csv_download=True,\n        chart=True,\n        redraw_chart=True,\n        auto_fix_sql=True,\n        ask_results_correct=True,\n        followup_questions=True,\n        summarization=True,\n        function_generation=True,\n        index_html_path=None,\n        assets_folder=None,\n    ):\n        \"\"\"\n        Expose a Flask app that can be used to interact with a Vanna instance.\n\n        Args:\n            vn: The Vanna instance to interact with.\n            cache: The cache to use. Defaults to MemoryCache, which uses an in-memory cache. You can also pass in a custom cache that implements the Cache interface.\n            auth: The authentication method to use. Defaults to NoAuth, which doesn't require authentication. You can also pass in a custom authentication method that implements the AuthInterface interface.\n            debug: Show the debug console. Defaults to True.\n            allow_llm_to_see_data: Whether to allow the LLM to see data. Defaults to False.\n            logo: The logo to display in the UI. Defaults to the Vanna logo.\n            title: The title to display in the UI. Defaults to \"Welcome to Vanna.AI\".\n            subtitle: The subtitle to display in the UI. Defaults to \"Your AI-powered copilot for SQL queries.\".\n            show_training_data: Whether to show the training data in the UI. Defaults to True.\n            suggested_questions: Whether to show suggested questions in the UI. Defaults to True.\n            sql: Whether to show the SQL input in the UI. Defaults to True.\n            table: Whether to show the table output in the UI. Defaults to True.\n            csv_download: Whether to allow downloading the table output as a CSV file. Defaults to True.\n            chart: Whether to show the chart output in the UI. Defaults to True.\n            redraw_chart: Whether to allow redrawing the chart. Defaults to True.\n            auto_fix_sql: Whether to allow auto-fixing SQL errors. Defaults to True.\n            ask_results_correct: Whether to ask the user if the results are correct. Defaults to True.\n            followup_questions: Whether to show followup questions. Defaults to True.\n            summarization: Whether to show summarization. Defaults to True.\n            index_html_path: Path to the index.html. Defaults to None, which will use the default index.html\n            assets_folder: The location where you'd like to serve the static assets from. Defaults to None, which will use hardcoded Python variables.\n\n        Returns:\n            None\n        \"\"\"\n        super().__init__(vn, cache, auth, debug, allow_llm_to_see_data, chart)\n\n        self.config[\"logo\"] = logo\n        self.config[\"title\"] = title\n        self.config[\"subtitle\"] = subtitle\n        self.config[\"show_training_data\"] = show_training_data\n        self.config[\"suggested_questions\"] = suggested_questions\n        self.config[\"sql\"] = sql\n        self.config[\"table\"] = table\n        self.config[\"csv_download\"] = csv_download\n        self.config[\"chart\"] = chart\n        self.config[\"redraw_chart\"] = redraw_chart\n        self.config[\"auto_fix_sql\"] = auto_fix_sql\n        self.config[\"ask_results_correct\"] = ask_results_correct\n        self.config[\"followup_questions\"] = followup_questions\n        self.config[\"summarization\"] = summarization\n        self.config[\"function_generation\"] = function_generation and hasattr(\n            vn, \"get_function\"\n        )\n        self.config[\"version\"] = importlib.metadata.version(\"vanna\")\n\n        self.index_html_path = index_html_path\n        self.assets_folder = assets_folder\n\n        @self.flask_app.route(\"/auth/login\", methods=[\"POST\"])\n        def login():\n            return self.auth.login_handler(flask.request)\n\n        @self.flask_app.route(\"/auth/callback\", methods=[\"GET\"])\n        def callback():\n            return self.auth.callback_handler(flask.request)\n\n        @self.flask_app.route(\"/auth/logout\", methods=[\"GET\"])\n        def logout():\n            return self.auth.logout_handler(flask.request)\n\n        @self.flask_app.route(\"/assets/<path:filename>\")\n        def proxy_assets(filename):\n            if self.assets_folder:\n                return send_from_directory(self.assets_folder, filename)\n\n            if \".css\" in filename:\n                return Response(css_content, mimetype=\"text/css\")\n\n            if \".js\" in filename:\n                return Response(js_content, mimetype=\"text/javascript\")\n\n            # Return 404\n            return \"File not found\", 404\n\n        # Proxy the /vanna.svg file to the remote server\n        @self.flask_app.route(\"/vanna.svg\")\n        def proxy_vanna_svg():\n            remote_url = \"https://vanna.ai/img/vanna.svg\"\n            response = requests.get(remote_url, stream=True)\n\n            # Check if the request to the remote URL was successful\n            if response.status_code == 200:\n                excluded_headers = [\n                    \"content-encoding\",\n                    \"content-length\",\n                    \"transfer-encoding\",\n                    \"connection\",\n                ]\n                headers = [\n                    (name, value)\n                    for (name, value) in response.raw.headers.items()\n                    if name.lower() not in excluded_headers\n                ]\n                return Response(response.content, response.status_code, headers)\n            else:\n                return \"Error fetching file from remote server\", response.status_code\n\n        @self.flask_app.route(\"/\", defaults={\"path\": \"\"})\n        @self.flask_app.route(\"/<path:path>\")\n        def hello(path: str):\n            if self.index_html_path:\n                directory = os.path.dirname(self.index_html_path)\n                filename = os.path.basename(self.index_html_path)\n                return send_from_directory(directory=directory, path=filename)\n            return html_content\n"
  },
  {
    "path": "src/vanna/legacy/flask/assets.py",
    "content": "html_content = \"\"\"<!doctype html>\n<html lang=\"en\" translate>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vanna.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <link href=\"https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@350&display=swap\" rel=\"stylesheet\">\n    <script src=\"https://cdn.plot.ly/plotly-latest.min.js\" type=\"text/javascript\"></script>\n    <title>Vanna.AI</title>\n    <script type=\"module\" crossorigin src=\"/assets/index-35bab439.js\"></script>\n    <link rel=\"stylesheet\" href=\"/assets/index-f228f78f.css\">\n  </head>\n  <body class=\"bg-white dark:bg-slate-900\">\n    <div id=\"app\"></div>\n    \n  </body>\n</html>\n\"\"\"\n\ncss_content = \"\"\".nav-title{font-family:Roboto Slab,serif}*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: \"\"}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",Segoe UI Symbol,\"Noto Color Emoji\";font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.inset-x-px{left:1px;right:1px}.inset-y-0{top:0;bottom:0}.bottom-0{bottom:0}.bottom-px{bottom:1px}.end-0{inset-inline-end:0px}.left-0{left:0}.right-0{right:0}.top-0{top:0}.z-10{z-index:10}.z-50{z-index:50}.z-\\\\[60\\\\]{z-index:60}.z-\\\\[80\\\\]{z-index:80}.-m-1{margin:-.25rem}.-m-1\\\\.5{margin:-.375rem}.m-1{margin:.25rem}.m-3{margin:.75rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-2\\\\.5{margin-bottom:.625rem}.mb-3{margin-bottom:.75rem}.mb-auto{margin-bottom:auto}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.mr-1{margin-right:.25rem}.mr-1\\\\.5{margin-right:.375rem}.mr-3{margin-right:.75rem}.ms-0{margin-inline-start:0px}.ms-3{margin-inline-start:.75rem}.mt-0{margin-top:0}.mt-0\\\\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-16{margin-top:4rem}.mt-2{margin-top:.5rem}.mt-2\\\\.5{margin-top:.625rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-7{margin-top:1.75rem}.mt-auto{margin-top:auto}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-1{height:.25rem}.h-1\\\\.5{height:.375rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-3\\\\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-52{height:13rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-\\\\[2\\\\.375rem\\\\]{height:2.375rem}.h-auto{height:auto}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.min-h-\\\\[15rem\\\\]{min-height:15rem}.min-h-\\\\[calc\\\\(100\\\\%-3\\\\.5rem\\\\)\\\\]{min-height:calc(100% - 3.5rem)}.w-0{width:0px}.w-1{width:.25rem}.w-1\\\\.5{width:.375rem}.w-2{width:.5rem}.w-28{width:7rem}.w-3{width:.75rem}.w-3\\\\.5{width:.875rem}.w-4{width:1rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-8{width:2rem}.w-\\\\[2\\\\.375rem\\\\]{width:2.375rem}.w-\\\\[3\\\\.25rem\\\\]{width:3.25rem}.w-full{width:100%}.w-px{width:1px}.min-w-full{min-width:100%}.max-w-2xl{max-width:42rem}.max-w-4xl{max-width:56rem}.max-w-7xl{max-width:80rem}.max-w-\\\\[50rem\\\\]{max-width:50rem}.max-w-\\\\[85rem\\\\]{max-width:85rem}.max-w-fit{max-width:-moz-fit-content;max-width:fit-content}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-auto{flex:1 1 auto}.flex-none{flex:none}.flex-shrink-0,.shrink-0{flex-shrink:0}.flex-grow,.grow{flex-grow:1}.-translate-x-full{--tw-translate-x: -100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-full{--tw-translate-x: 100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes bounce{0%,to{transform:translateY(-25%);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;animation-timing-function:cubic-bezier(0,0,.2,1)}}.animate-bounce{animation:bounce 1s infinite}@keyframes ping{75%,to{transform:scale(2);opacity:0}}.animate-ping{animation:ping 1s cubic-bezier(0,0,.2,1) infinite}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.resize{resize:both}.list-disc{list-style-type:disc}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\\\\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-x-1{-moz-column-gap:.25rem;column-gap:.25rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-y-4{row-gap:1rem}.-space-y-px>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(-1px * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(-1px * var(--tw-space-y-reverse))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\\\\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}.divide-x>:not([hidden])~:not([hidden]){--tw-divide-x-reverse: 0;border-right-width:calc(1px * var(--tw-divide-x-reverse));border-left-width:calc(1px * calc(1 - var(--tw-divide-x-reverse)))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(229 231 235 / var(--tw-divide-opacity))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-hidden{overflow-y:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.whitespace-break-spaces{white-space:break-spaces}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-b-md{border-bottom-right-radius:.375rem;border-bottom-left-radius:.375rem}.rounded-t-xl{border-top-left-radius:.75rem;border-top-right-radius:.75rem}.rounded-ee-xl{border-end-end-radius:.75rem}.rounded-es-xl{border-end-start-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-4{border-width:4px}.border-\\\\[3px\\\\]{border-width:3px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-r{border-right-width:1px}.border-s{border-inline-start-width:1px}.border-t{border-top-width:1px}.border-t-2{border-top-width:2px}.border-t-4{border-top-width:4px}.border-blue-500{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity))}.border-blue-600{--tw-border-opacity: 1;border-color:rgb(37 99 235 / var(--tw-border-opacity))}.border-current{border-color:currentColor}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity))}.border-gray-600{--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity))}.border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity))}.border-green-200{--tw-border-opacity: 1;border-color:rgb(187 247 208 / var(--tw-border-opacity))}.border-neutral-700{--tw-border-opacity: 1;border-color:rgb(64 64 64 / var(--tw-border-opacity))}.border-red-200{--tw-border-opacity: 1;border-color:rgb(254 202 202 / var(--tw-border-opacity))}.border-red-500{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity))}.border-red-600{--tw-border-opacity: 1;border-color:rgb(220 38 38 / var(--tw-border-opacity))}.border-teal-100{--tw-border-opacity: 1;border-color:rgb(204 251 241 / var(--tw-border-opacity))}.border-teal-500{--tw-border-opacity: 1;border-color:rgb(20 184 166 / var(--tw-border-opacity))}.border-teal-900{--tw-border-opacity: 1;border-color:rgb(19 78 74 / var(--tw-border-opacity))}.border-transparent{border-color:transparent}.border-yellow-200{--tw-border-opacity: 1;border-color:rgb(254 240 138 / var(--tw-border-opacity))}.border-t-blue-500{--tw-border-opacity: 1;border-top-color:rgb(59 130 246 / var(--tw-border-opacity))}.border-t-blue-600{--tw-border-opacity: 1;border-top-color:rgb(37 99 235 / var(--tw-border-opacity))}.border-t-green-500{--tw-border-opacity: 1;border-top-color:rgb(34 197 94 / var(--tw-border-opacity))}.border-t-green-600{--tw-border-opacity: 1;border-top-color:rgb(22 163 74 / var(--tw-border-opacity))}.border-t-red-500{--tw-border-opacity: 1;border-top-color:rgb(239 68 68 / var(--tw-border-opacity))}.border-t-red-600{--tw-border-opacity: 1;border-top-color:rgb(220 38 38 / var(--tw-border-opacity))}.border-t-transparent{border-top-color:transparent}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity))}.bg-gray-600{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity))}.bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity))}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity))}.bg-neutral-800{--tw-bg-opacity: 1;background-color:rgb(38 38 38 / var(--tw-bg-opacity))}.bg-neutral-900{--tw-bg-opacity: 1;background-color:rgb(23 23 23 / var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}.bg-slate-800{--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity))}.bg-slate-900{--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity))}.bg-teal-200{--tw-bg-opacity: 1;background-color:rgb(153 246 228 / var(--tw-bg-opacity))}.bg-teal-50{--tw-bg-opacity: 1;background-color:rgb(240 253 250 / var(--tw-bg-opacity))}.bg-teal-800{--tw-bg-opacity: 1;background-color:rgb(17 94 89 / var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-yellow-50{--tw-bg-opacity: 1;background-color:rgb(254 252 232 / var(--tw-bg-opacity))}.bg-opacity-50{--tw-bg-opacity: .5}.bg-opacity-80{--tw-bg-opacity: .8}.p-1{padding:.25rem}.p-1\\\\.5{padding:.375rem}.p-2{padding:.5rem}.p-2\\\\.5{padding:.625rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-7{padding-left:1.75rem;padding-right:1.75rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\\\\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.pb-12{padding-bottom:3rem}.pe-11{padding-inline-end:2.75rem}.pe-3{padding-inline-end:.75rem}.pl-3{padding-left:.75rem}.pl-7{padding-left:1.75rem}.pr-10{padding-right:2.5rem}.pr-4{padding-right:1rem}.pr-9{padding-right:2.25rem}.ps-5{padding-inline-start:1.25rem}.text-left{text-align:left}.text-center{text-align:center}.text-start{text-align:start}.align-middle{vertical-align:middle}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-7xl{font-size:4.5rem;line-height:1}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-none{line-height:1}.tracking-wide{letter-spacing:.025em}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity))}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity))}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity))}.text-neutral-200{--tw-text-opacity: 1;color:rgb(229 229 229 / var(--tw-text-opacity))}.text-neutral-300{--tw-text-opacity: 1;color:rgb(212 212 212 / var(--tw-text-opacity))}.text-neutral-400{--tw-text-opacity: 1;color:rgb(163 163 163 / var(--tw-text-opacity))}.text-neutral-500{--tw-text-opacity: 1;color:rgb(115 115 115 / var(--tw-text-opacity))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity))}.text-slate-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.text-slate-700{--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity))}.text-teal-400{--tw-text-opacity: 1;color:rgb(45 212 191 / var(--tw-text-opacity))}.text-teal-800{--tw-text-opacity: 1;color:rgb(17 94 89 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity))}.text-yellow-700{--tw-text-opacity: 1;color:rgb(161 98 7 / var(--tw-text-opacity))}.text-yellow-800{--tw-text-opacity: 1;color:rgb(133 77 14 / var(--tw-text-opacity))}.decoration-2{text-decoration-thickness:2px}.opacity-0{opacity:0}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-neutral-700{--tw-shadow-color: #404040;--tw-shadow: var(--tw-shadow-colored)}.shadow-slate-700{--tw-shadow-color: #334155;--tw-shadow: var(--tw-shadow-colored)}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-transparent{--tw-ring-color: transparent}.ring-offset-white{--tw-ring-offset-color: #fff}.blur{--tw-blur: blur(8px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.\\\\[--body-scroll\\\\:true\\\\]{--body-scroll: true}.marker\\\\:text-blue-600 *::marker{color:#2563eb}.marker\\\\:text-blue-600::marker{color:#2563eb}.before\\\\:inline-block:before{content:var(--tw-content);display:inline-block}.before\\\\:h-6:before{content:var(--tw-content);height:1.5rem}.before\\\\:w-6:before{content:var(--tw-content);width:1.5rem}.before\\\\:translate-x-0:before{content:var(--tw-content);--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.before\\\\:transform:before{content:var(--tw-content);transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.before\\\\:rounded-full:before{content:var(--tw-content);border-radius:9999px}.before\\\\:bg-white:before{content:var(--tw-content);--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.before\\\\:shadow:before{content:var(--tw-content);--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.before\\\\:ring-0:before{content:var(--tw-content);--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.before\\\\:transition:before{content:var(--tw-content);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.before\\\\:duration-200:before{content:var(--tw-content);transition-duration:.2s}.before\\\\:ease-in-out:before{content:var(--tw-content);transition-timing-function:cubic-bezier(.4,0,.2,1)}.first\\\\:mt-0:first-child{margin-top:0}.first\\\\:rounded-t-lg:first-child{border-top-left-radius:.5rem;border-top-right-radius:.5rem}.last\\\\:rounded-b-lg:last-child{border-bottom-right-radius:.5rem;border-bottom-left-radius:.5rem}.checked\\\\:bg-blue-600:checked{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity))}.checked\\\\:bg-none:checked{background-image:none}.checked\\\\:before\\\\:translate-x-full:checked:before{content:var(--tw-content);--tw-translate-x: 100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.checked\\\\:before\\\\:bg-blue-200:checked:before{content:var(--tw-content);--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity))}.hover\\\\:border-blue-500:hover{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity))}.hover\\\\:border-green-500:hover{--tw-border-opacity: 1;border-color:rgb(34 197 94 / var(--tw-border-opacity))}.hover\\\\:border-red-400:hover{--tw-border-opacity: 1;border-color:rgb(248 113 113 / var(--tw-border-opacity))}.hover\\\\:border-red-500:hover{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity))}.hover\\\\:bg-blue-50:hover{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity))}.hover\\\\:bg-blue-500:hover{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity))}.hover\\\\:bg-blue-600:hover{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity))}.hover\\\\:bg-blue-700:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity))}.hover\\\\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.hover\\\\:bg-gray-50:hover{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity))}.hover\\\\:bg-green-500:hover{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity))}.hover\\\\:bg-red-500:hover{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}.hover\\\\:text-blue-500:hover{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity))}.hover\\\\:text-blue-600:hover{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity))}.hover\\\\:text-blue-800:hover{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))}.hover\\\\:text-gray-400:hover{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}.hover\\\\:text-green-800:hover{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity))}.hover\\\\:text-red-400:hover{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity))}.hover\\\\:text-red-500:hover{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity))}.hover\\\\:text-red-600:hover{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity))}.hover\\\\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.focus\\\\:z-10:focus{z-index:10}.focus\\\\:border-blue-500:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity))}.focus\\\\:border-blue-600:focus{--tw-border-opacity: 1;border-color:rgb(37 99 235 / var(--tw-border-opacity))}.focus\\\\:border-indigo-500:focus{--tw-border-opacity: 1;border-color:rgb(99 102 241 / var(--tw-border-opacity))}.focus\\\\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\\\\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\\\\:ring-blue-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity))}.focus\\\\:ring-blue-600:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(37 99 235 / var(--tw-ring-opacity))}.focus\\\\:ring-gray-400:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(156 163 175 / var(--tw-ring-opacity))}.focus\\\\:ring-green-200:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(187 247 208 / var(--tw-ring-opacity))}.focus\\\\:ring-indigo-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity))}.focus\\\\:ring-red-200:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(254 202 202 / var(--tw-ring-opacity))}.focus\\\\:ring-red-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity))}.focus\\\\:ring-offset-2:focus{--tw-ring-offset-width: 2px}.focus\\\\:ring-offset-white:focus{--tw-ring-offset-color: #fff}.disabled\\\\:pointer-events-none:disabled{pointer-events:none}.disabled\\\\:opacity-50:disabled{opacity:.5}[data-hs-tab].active.hs-tab-active\\\\:border-blue-600{--tw-border-opacity: 1;border-color:rgb(37 99 235 / var(--tw-border-opacity))}[data-hs-tab].active.hs-tab-active\\\\:font-semibold{font-weight:600}[data-hs-tab].active.hs-tab-active\\\\:text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity))}[data-hs-tab].active .hs-tab-active\\\\:border-blue-600{--tw-border-opacity: 1;border-color:rgb(37 99 235 / var(--tw-border-opacity))}[data-hs-tab].active .hs-tab-active\\\\:font-semibold{font-weight:600}[data-hs-tab].active .hs-tab-active\\\\:text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity))}.open.hs-overlay-open\\\\:mt-7{margin-top:1.75rem}.open.hs-overlay-open\\\\:translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.open.hs-overlay-open\\\\:opacity-100{opacity:1}.open.hs-overlay-open\\\\:duration-500{transition-duration:.5s}.open .hs-overlay-open\\\\:mt-7{margin-top:1.75rem}.open .hs-overlay-open\\\\:translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.open .hs-overlay-open\\\\:opacity-100{opacity:1}.open .hs-overlay-open\\\\:duration-500{transition-duration:.5s}@media (prefers-color-scheme: dark){.dark\\\\:divide-gray-700>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(55 65 81 / var(--tw-divide-opacity))}.dark\\\\:border-blue-500{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity))}.dark\\\\:border-gray-600{--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity))}.dark\\\\:border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity))}.dark\\\\:border-neutral-700{--tw-border-opacity: 1;border-color:rgb(64 64 64 / var(--tw-border-opacity))}.dark\\\\:border-red-500{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity))}.dark\\\\:border-teal-900{--tw-border-opacity: 1;border-color:rgb(19 78 74 / var(--tw-border-opacity))}.dark\\\\:border-t-blue-500{--tw-border-opacity: 1;border-top-color:rgb(59 130 246 / var(--tw-border-opacity))}.dark\\\\:border-t-green-500{--tw-border-opacity: 1;border-top-color:rgb(34 197 94 / var(--tw-border-opacity))}.dark\\\\:border-t-red-500{--tw-border-opacity: 1;border-top-color:rgb(239 68 68 / var(--tw-border-opacity))}.dark\\\\:bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity))}.dark\\\\:bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity))}.dark\\\\:bg-neutral-800{--tw-bg-opacity: 1;background-color:rgb(38 38 38 / var(--tw-bg-opacity))}.dark\\\\:bg-neutral-900{--tw-bg-opacity: 1;background-color:rgb(23 23 23 / var(--tw-bg-opacity))}.dark\\\\:bg-slate-800{--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity))}.dark\\\\:bg-slate-900{--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity))}.dark\\\\:bg-teal-800{--tw-bg-opacity: 1;background-color:rgb(17 94 89 / var(--tw-bg-opacity))}.dark\\\\:bg-teal-800\\\\/30{background-color:#115e594d}.dark\\\\:bg-opacity-80{--tw-bg-opacity: .8}.dark\\\\:text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity))}.dark\\\\:text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity))}.dark\\\\:text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity))}.dark\\\\:text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}.dark\\\\:text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.dark\\\\:text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity))}.dark\\\\:text-neutral-200{--tw-text-opacity: 1;color:rgb(229 229 229 / var(--tw-text-opacity))}.dark\\\\:text-neutral-400{--tw-text-opacity: 1;color:rgb(163 163 163 / var(--tw-text-opacity))}.dark\\\\:text-neutral-500{--tw-text-opacity: 1;color:rgb(115 115 115 / var(--tw-text-opacity))}.dark\\\\:text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity))}.dark\\\\:text-slate-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.dark\\\\:text-teal-400{--tw-text-opacity: 1;color:rgb(45 212 191 / var(--tw-text-opacity))}.dark\\\\:text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.dark\\\\:placeholder-gray-400::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(156 163 175 / var(--tw-placeholder-opacity))}.dark\\\\:placeholder-gray-400::placeholder{--tw-placeholder-opacity: 1;color:rgb(156 163 175 / var(--tw-placeholder-opacity))}.dark\\\\:placeholder-neutral-500::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(115 115 115 / var(--tw-placeholder-opacity))}.dark\\\\:placeholder-neutral-500::placeholder{--tw-placeholder-opacity: 1;color:rgb(115 115 115 / var(--tw-placeholder-opacity))}.dark\\\\:shadow-neutral-700\\\\/70{--tw-shadow-color: rgb(64 64 64 / .7);--tw-shadow: var(--tw-shadow-colored)}.dark\\\\:shadow-slate-700\\\\/\\\\[\\\\.7\\\\]{--tw-shadow-color: rgb(51 65 85 / .7);--tw-shadow: var(--tw-shadow-colored)}.dark\\\\:before\\\\:bg-gray-400:before{content:var(--tw-content);--tw-bg-opacity: 1;background-color:rgb(156 163 175 / var(--tw-bg-opacity))}.dark\\\\:checked\\\\:border-blue-500:checked{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity))}.dark\\\\:checked\\\\:bg-blue-500:checked{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity))}.dark\\\\:checked\\\\:bg-blue-600:checked{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity))}.dark\\\\:checked\\\\:before\\\\:bg-blue-200:checked:before{content:var(--tw-content);--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity))}.dark\\\\:hover\\\\:border-blue-400:hover{--tw-border-opacity: 1;border-color:rgb(96 165 250 / var(--tw-border-opacity))}.dark\\\\:hover\\\\:border-red-400:hover{--tw-border-opacity: 1;border-color:rgb(248 113 113 / var(--tw-border-opacity))}.dark\\\\:hover\\\\:bg-gray-900:hover{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity))}.dark\\\\:hover\\\\:bg-neutral-700:hover{--tw-bg-opacity: 1;background-color:rgb(64 64 64 / var(--tw-bg-opacity))}.dark\\\\:hover\\\\:bg-slate-800:hover{--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity))}.dark\\\\:hover\\\\:text-blue-400:hover{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity))}.dark\\\\:hover\\\\:text-blue-500:hover{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity))}.dark\\\\:hover\\\\:text-green-400:hover{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity))}.dark\\\\:hover\\\\:text-red-400:hover{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity))}.dark\\\\:hover\\\\:text-red-500:hover{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity))}.dark\\\\:hover\\\\:text-slate-300:hover{--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity))}.dark\\\\:hover\\\\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.dark\\\\:focus\\\\:border-blue-500:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity))}.dark\\\\:focus\\\\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.dark\\\\:focus\\\\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.dark\\\\:focus\\\\:ring-blue-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity))}.dark\\\\:focus\\\\:ring-gray-600:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(75 85 99 / var(--tw-ring-opacity))}.dark\\\\:focus\\\\:ring-gray-700:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(55 65 81 / var(--tw-ring-opacity))}.dark\\\\:focus\\\\:ring-neutral-600:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(82 82 82 / var(--tw-ring-opacity))}.dark\\\\:focus\\\\:ring-red-600:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(220 38 38 / var(--tw-ring-opacity))}.dark\\\\:focus\\\\:ring-offset-gray-800:focus{--tw-ring-offset-color: #1f2937}}@media (min-width: 640px){.sm\\\\:mx-auto{margin-left:auto;margin-right:auto}.sm\\\\:mb-3{margin-bottom:.75rem}.sm\\\\:mt-10{margin-top:2.5rem}.sm\\\\:w-auto{width:auto}.sm\\\\:w-full{width:100%}.sm\\\\:max-w-lg{max-width:32rem}.sm\\\\:flex-row{flex-direction:row}.sm\\\\:gap-3{gap:.75rem}.sm\\\\:gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.sm\\\\:space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.sm\\\\:p-4{padding:1rem}.sm\\\\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\\\\:py-4{padding-top:1rem;padding-bottom:1rem}.sm\\\\:py-6{padding-top:1.5rem;padding-bottom:1.5rem}.sm\\\\:text-3xl{font-size:1.875rem;line-height:2.25rem}.sm\\\\:text-4xl{font-size:2.25rem;line-height:2.5rem}.sm\\\\:text-9xl{font-size:8rem;line-height:1}.sm\\\\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width: 768px){.md\\\\:flex{display:flex}.md\\\\:items-center{align-items:center}.md\\\\:justify-between{justify-content:space-between}.md\\\\:p-10{padding:2.5rem}.md\\\\:p-5{padding:1.25rem}}@media (min-width: 1024px){.lg\\\\:bottom-0{bottom:0}.lg\\\\:right-auto{right:auto}.lg\\\\:block{display:block}.lg\\\\:hidden{display:none}.lg\\\\:translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.lg\\\\:px-8{padding-left:2rem;padding-right:2rem}.lg\\\\:py-14{padding-top:3.5rem;padding-bottom:3.5rem}.lg\\\\:pl-64{padding-left:16rem}}\n\"\"\"\n\njs_content = '''var Rn=Object.defineProperty;var nn=(E,e,T)=>e in E?Rn(E,e,{enumerable:!0,configurable:!0,writable:!0,value:T}):E[e]=T;var wt=(E,e,T)=>(nn(E,typeof e!=\"symbol\"?e+\"\":e,T),T);(function(){const e=document.createElement(\"link\").relList;if(e&&e.supports&&e.supports(\"modulepreload\"))return;for(const r of document.querySelectorAll('link[rel=\"modulepreload\"]'))t(r);new MutationObserver(r=>{for(const R of r)if(R.type===\"childList\")for(const n of R.addedNodes)n.tagName===\"LINK\"&&n.rel===\"modulepreload\"&&t(n)}).observe(document,{childList:!0,subtree:!0});function T(r){const R={};return r.integrity&&(R.integrity=r.integrity),r.referrerPolicy&&(R.referrerPolicy=r.referrerPolicy),r.crossOrigin===\"use-credentials\"?R.credentials=\"include\":r.crossOrigin===\"anonymous\"?R.credentials=\"omit\":R.credentials=\"same-origin\",R}function t(r){if(r.ep)return;r.ep=!0;const R=T(r);fetch(r.href,R)}})();function j(){}function An(E,e){for(const T in e)E[T]=e[T];return E}function cR(E){return E()}function YT(){return Object.create(null)}function NE(E){E.forEach(cR)}function zE(E){return typeof E==\"function\"}function _e(E,e){return E!=E?e==e:E!==e||E&&typeof E==\"object\"||typeof E==\"function\"}let ut;function VT(E,e){return E===e?!0:(ut||(ut=document.createElement(\"a\")),ut.href=e,E===ut.href)}function sn(E){return Object.keys(E).length===0}function fR(E,...e){if(E==null){for(const t of e)t(void 0);return j}const T=E.subscribe(...e);return T.unsubscribe?()=>T.unsubscribe():T}function uE(E){let e;return fR(E,T=>e=T)(),e}function eE(E,e,T){E.$$.on_destroy.push(fR(e,T))}function Ut(E,e,T,t){if(E){const r=PR(E,e,T,t);return E[0](r)}}function PR(E,e,T,t){return E[1]&&t?An(T.ctx.slice(),E[1](t(e))):T.ctx}function mt(E,e,T,t){if(E[2]&&t){const r=E[2](t(T));if(e.dirty===void 0)return r;if(typeof r==\"object\"){const R=[],n=Math.max(e.dirty.length,r.length);for(let s=0;s<n;s+=1)R[s]=e.dirty[s]|r[s];return R}return e.dirty|r}return e.dirty}function ht(E,e,T,t,r,R){if(r){const n=PR(e,T,t,R);E.p(n,r)}}function Gt(E){if(E.ctx.length>32){const e=[],T=E.ctx.length/32;for(let t=0;t<T;t++)e[t]=-1;return e}return-1}function OT(E,e,T){return E.set(T),e}function l(E,e){E.appendChild(e)}function V(E,e,T){E.insertBefore(e,T||null)}function Y(E){E.parentNode&&E.parentNode.removeChild(E)}function nE(E,e){for(let T=0;T<E.length;T+=1)E[T]&&E[T].d(e)}function f(E){return document.createElement(E)}function OE(E){return document.createElementNS(\"http://www.w3.org/2000/svg\",E)}function te(E){return document.createTextNode(E)}function $(){return te(\" \")}function je(){return te(\"\")}function Ne(E,e,T,t){return E.addEventListener(e,T,t),()=>E.removeEventListener(e,T,t)}function a(E,e,T){T==null?E.removeAttribute(e):E.getAttribute(e)!==T&&E.setAttribute(e,T)}function Sn(E){let e;return{p(...T){e=T,e.forEach(t=>E.push(t))},r(){e.forEach(T=>E.splice(E.indexOf(T),1))}}}function on(E){return Array.from(E.childNodes)}function Le(E,e){e=\"\"+e,E.data!==e&&(E.data=e)}function Ye(E,e){E.value=e??\"\"}function ct(E,e,T,t){T==null?E.style.removeProperty(e):E.style.setProperty(e,T,t?\"important\":\"\")}function WT(E,e,T){for(let t=0;t<E.options.length;t+=1){const r=E.options[t];if(r.__value===e){r.selected=!0;return}}(!T||e!==void 0)&&(E.selectedIndex=-1)}function On(E){const e=E.querySelector(\":checked\");return e&&e.__value}let Ot;function st(E){Ot=E}function an(){if(!Ot)throw new Error(\"Function called outside component initialization\");return Ot}function DR(E){an().$$.on_mount.push(E)}const jE=[],iT=[];let et=[];const aT=[],In=Promise.resolve();let IT=!1;function Nn(){IT||(IT=!0,In.then(dR))}function dt(E){et.push(E)}function ln(E){aT.push(E)}const $t=new Set;let JE=0;function dR(){if(JE!==0)return;const E=Ot;do{try{for(;JE<jE.length;){const e=jE[JE];JE++,st(e),_n(e.$$)}}catch(e){throw jE.length=0,JE=0,e}for(st(null),jE.length=0,JE=0;iT.length;)iT.pop()();for(let e=0;e<et.length;e+=1){const T=et[e];$t.has(T)||($t.add(T),T())}et.length=0}while(jE.length);for(;aT.length;)aT.pop()();IT=!1,$t.clear(),st(E)}function _n(E){if(E.fragment!==null){E.update(),NE(E.before_update);const e=E.dirty;E.dirty=[-1],E.fragment&&E.fragment.p(E.ctx,e),E.after_update.forEach(dt)}}function Ln(E){const e=[],T=[];et.forEach(t=>E.indexOf(t)===-1?e.push(t):T.push(t)),T.forEach(t=>t()),et=e}const ft=new Set;let vE;function Ge(){vE={r:0,c:[],p:vE}}function ge(){vE.r||NE(vE.c),vE=vE.p}function m(E,e){E&&E.i&&(ft.delete(E),E.i(e))}function y(E,e,T,t){if(E&&E.o){if(ft.has(E))return;ft.add(E),vE.c.push(()=>{ft.delete(E),t&&(T&&E.d(1),t())}),E.o(e)}else t&&t()}function De(E){return(E==null?void 0:E.length)!==void 0?E:Array.from(E)}function Cn(E,e){E.d(1),e.delete(E.key)}function un(E,e,T,t,r,R,n,s,S,A,o,i){let _=E.length,c=R.length,P=_;const p={};for(;P--;)p[E[P].key]=P;const C=[],L=new Map,I=new Map,u=[];for(P=c;P--;){const O=i(r,R,P),N=T(O);let D=n.get(N);D?t&&u.push(()=>D.p(O,e)):(D=A(N,O),D.c()),L.set(N,C[P]=D),N in p&&I.set(N,Math.abs(P-p[N]))}const H=new Set,b=new Set;function M(O){m(O,1),O.m(s,o),n.set(O.key,O),o=O.first,c--}for(;_&&c;){const O=C[c-1],N=E[_-1],D=O.key,B=N.key;O===N?(o=O.first,_--,c--):L.has(B)?!n.has(D)||H.has(D)?M(O):b.has(B)?_--:I.get(D)>I.get(B)?(b.add(D),M(O)):(H.add(B),_--):(S(N,n),_--)}for(;_--;){const O=E[_];L.has(O.key)||S(O,n)}for(;c;)M(C[c-1]);return NE(u),C}function cn(E,e,T){const t=E.$$.props[e];t!==void 0&&(E.$$.bound[t]=T,T(E.$$.ctx[t]))}function K(E){E&&E.c()}function X(E,e,T){const{fragment:t,after_update:r}=E.$$;t&&t.m(e,T),dt(()=>{const R=E.$$.on_mount.map(cR).filter(zE);E.$$.on_destroy?E.$$.on_destroy.push(...R):NE(R),E.$$.on_mount=[]}),r.forEach(dt)}function k(E,e){const T=E.$$;T.fragment!==null&&(Ln(T.after_update),NE(T.on_destroy),T.fragment&&T.fragment.d(e),T.on_destroy=T.fragment=null,T.ctx=[])}function fn(E,e){E.$$.dirty[0]===-1&&(jE.push(E),Nn(),E.$$.dirty.fill(0)),E.$$.dirty[e/31|0]|=1<<e%31}function Ce(E,e,T,t,r,R,n,s=[-1]){const S=Ot;st(E);const A=E.$$={fragment:null,ctx:[],props:R,update:j,not_equal:r,bound:YT(),on_mount:[],on_destroy:[],on_disconnect:[],before_update:[],after_update:[],context:new Map(e.context||(S?S.$$.context:[])),callbacks:YT(),dirty:s,skip_bound:!1,root:e.target||S.$$.root};n&&n(A.root);let o=!1;if(A.ctx=T?T(E,e.props||{},(i,_,...c)=>{const P=c.length?c[0]:_;return A.ctx&&r(A.ctx[i],A.ctx[i]=P)&&(!A.skip_bound&&A.bound[i]&&A.bound[i](P),o&&fn(E,i)),_}):[],A.update(),o=!0,NE(A.before_update),A.fragment=t?t(A.ctx):!1,e.target){if(e.hydrate){const i=on(e.target);A.fragment&&A.fragment.l(i),i.forEach(Y)}else A.fragment&&A.fragment.c();e.intro&&m(E.$$.fragment),X(E,e.target,e.anchor),dR()}st(S)}class ue{constructor(){wt(this,\"$$\");wt(this,\"$$set\")}$destroy(){k(this,1),this.$destroy=j}$on(e,T){if(!zE(T))return j;const t=this.$$.callbacks[e]||(this.$$.callbacks[e]=[]);return t.push(T),()=>{const r=t.indexOf(T);r!==-1&&t.splice(r,1)}}$set(e){this.$$set&&!sn(e)&&(this.$$.skip_bound=!0,this.$$set(e),this.$$.skip_bound=!1)}}const Pn=\"4\";typeof window<\"u\"&&(window.__svelte||(window.__svelte={v:new Set})).v.add(Pn);const qE=[];function iE(E,e=j){let T;const t=new Set;function r(s){if(_e(E,s)&&(E=s,T)){const S=!qE.length;for(const A of t)A[1](),qE.push(A,E);if(S){for(let A=0;A<qE.length;A+=2)qE[A][0](qE[A+1]);qE.length=0}}}function R(s){r(s(E))}function n(s,S=j){const A=[s,S];return t.add(A),t.size===1&&(T=e(r,R)||j),s(E),()=>{t.delete(A),t.size===0&&T&&(T(),T=null)}}return{set:r,update:R,subscribe:n}}let GE=iE(\"\"),YE=iE([]),LT=iE(null),gt=iE(null),Ht=iE(!1),St=iE(!1),DE=iE(\"chat\"),CT=iE([]),Et=iE(\"\"),pR=iE(!1),BE=iE(\"\"),VE=iE({debug:!0,logo:\"\",title:\"Welcome to Vanna.AI\",subtitle:\"Loading...\",show_training_data:!0,suggested_questions:!0,sql:!0,table:!0,csv_download:!0,chart:!0,redraw_chart:!0,auto_fix_sql:!0,ask_results_correct:!0,followup_questions:!0,summarization:!0,function_generation:!0,version:\"\"}),bt=iE(null),MR=iE([]);function UR(){YE.set([]),Ht.set(!1),St.set(!1)}async function uT(E){let e=uE(VE),T=yn();if(Se({type:\"user_question\",question:E}),Ht.set(!0),T){const n=await Pe(\"generate_rewritten_question\",\"GET\",{last_question:T,new_question:E});n.type===\"rewritten_question\"&&n.question!==E&&(Se(n),E=n.question)}const t=await Pe(\"get_function\",\"GET\",{question:E});let r;if(e.function_generation&&t.type===\"function\")Se(t),r=t.id,GE.set(t.id),Et.set(t.function.instantiated_sql);else{const n=await Pe(\"generate_sql\",\"GET\",{question:E});if(Se(n),n.type!==\"sql\")return;window.location.hash=n.id,GE.set(n.id),Et.set(n.text),r=n.id}const R=await Pe(\"run_sql\",\"GET\",{id:r});if(Se(R),R.type===\"df\"){if(R.should_generate_chart){const n=await Pe(\"generate_plotly_figure\",\"GET\",{id:R.id});if(Se(n),n.type!==\"plotly_figure\")return;CT.update(s=>[...s,{question:E,id:n.id}])}if(e.summarization){const n=await Pe(\"generate_summary\",\"GET\",{id:r});Se(n)}Se({type:\"feedback_question\"}),Se({type:\"feedback_buttons\"})}}async function Dn(E){let e=uE(VE);if(Se(E),E.type!==\"sql\")return;window.location.hash=E.id,GE.set(E.id),Et.set(E.text);const T=await Pe(\"run_sql\",\"GET\",{id:E.id});if(Se(T),T.type!==\"df\")return;const t=await Pe(\"generate_plotly_figure\",\"GET\",{id:T.id});if(Se(t),t.type===\"plotly_figure\"){if(e.summarization){const r=await Pe(\"generate_summary\",\"GET\",{id:t.id});Se(r)}Se({type:\"feedback_question\"}),Se({type:\"feedback_buttons\"})}}function dn(E){Se({type:\"user_question\",question:\"Re-run the SQL\"}),Pe(\"run_sql\",\"GET\",{id:E}).then(Se).then(e=>{e.type===\"df\"&&Pe(\"generate_plotly_figure\",\"GET\",{id:e.id}).then(Se).then(T=>{T.type===\"plotly_figure\"&&Pe(\"generate_followup_questions\",\"GET\",{id:T.id}).then(Se)})})}function mR(){Pe(\"get_question_history\",\"GET\",[]).then(gn)}function pn(){Pe(\"get_config\",\"GET\",[]).then(Gn)}function cT(){window.location.hash=\"functions\",DE.set(\"functions\"),Pe(\"get_all_functions\",\"GET\",[]).then(mn)}function hR(){window.location.hash=\"training-data\",DE.set(\"training-data\"),Pe(\"get_training_data\",\"GET\",[]).then(pt)}function it(){window.location.hash=\"\",DE.set(\"chat\"),UR(),uE(LT)===null&&Pe(\"generate_questions\",\"GET\",[]).then(hn),mR()}function Mn(E){window.location.hash=E,DE.set(\"chat\"),UR(),Ht.set(!0),Pe(\"load_question\",\"GET\",{id:E}).then(Se)}function Un(E){gt.set(null),Pe(\"remove_training_data\",\"POST\",{id:E}).then(e=>{Pe(\"get_training_data\",\"GET\",[]).then(pt)})}function Se(E){return E.type===\"not_logged_in\"?(bt.set(E.html),DE.set(\"login\"),E):(YE.update(e=>[...e,E]),bn(),E)}function pt(E){return gt.set(E),E.type===\"df\"?JSON.parse(E.df).length===0&&DE.set(\"no-training-data\"):E.type===\"not_logged_in\"&&(bt.set(E.html),DE.set(\"login\")),E}function mn(E){return E.type===\"functions\"&&MR.set(E.functions),E}function hn(E){return LT.set(E),E}function Gn(E){return E.type===\"config\"?(VE.set(E.config),E.config.debug&&xn()):E.type===\"not_logged_in\"&&(bt.set(E.html),DE.set(\"login\")),E}function gn(E){return E.type===\"question_history\"&&CT.set(E.questions),E}function Hn(E,e){gt.set(null);let T={};T[e]=E,Pe(\"train\",\"POST\",T).then(pt).then(t=>{t.type!==\"error\"&&Pe(\"get_training_data\",\"GET\",[]).then(pt)})}async function Pe(E,e,T){try{St.set(!0);let t=\"\",r;if(e===\"GET\")t=Object.entries(T).filter(([n,s])=>n!==\"endpoint\"&&n!==\"addMessage\").map(([n,s])=>`${encodeURIComponent(n)}=${encodeURIComponent(s)}`).join(\"&\"),r=await fetch(`/api/v0/${E}?${t}`);else{let n=JSON.stringify(T);r=await fetch(`/api/v0/${E}`,{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:n})}if(!r.ok)throw new Error(\"The server returned an error. See the server logs for more details. If you are running in Colab, this function is probably not supported. Please try running in a local environment.\");const R=await r.json();return St.set(!1),R}catch(t){return St.set(!1),{type:\"error\",error:String(t)}}}function bn(){setTimeout(()=>{window.scrollTo({top:document.body.scrollHeight,behavior:\"smooth\"})},100)}function yn(){let E=uE(YE),e=E.findLast(T=>T.type===\"rewritten_question\");return e||(e=E.findLast(T=>T.type===\"user_question\")),e&&(e.type===\"rewritten_question\"||e.type===\"user_question\")?e.question:null}function fT(){let E=uE(YE),e=E.find(T=>T.type===\"user_question\");if(e&&e.type===\"user_question\"){let T=E.findLast(t=>t.type===\"sql\");if(T&&T.type===\"sql\")return{question:e.question,sql:T.text}}return null}function at(E){YE.update(e=>e.filter(T=>T.type!==E))}function Bn(E){Pe(\"fix_sql\",\"POST\",{id:uE(GE),error:E}).then(Dn)}function vn(E){let T=uE(YE).find(t=>t.type===\"user_question\");T&&T.type===\"user_question\"&&(Pe(\"update_sql\",\"POST\",{id:uE(GE),sql:E}).then(Se).then(t=>{t.type===\"sql\"&&(Et.set(t.text),Pe(\"run_sql\",\"GET\",{id:t.id}).then(Se).then(r=>{r.type===\"df\"?JSON.parse(r.df).length>1?Pe(\"generate_plotly_figure\",\"GET\",{id:r.id}).then(Se).then(n=>{Se({type:\"feedback_question\"}),Se({type:\"feedback_buttons\"})}):(Se({type:\"feedback_question\"}),Se({type:\"feedback_buttons\"})):(Se({type:\"feedback_question\"}),Se({type:\"feedback_buttons\"}))}))}),at(\"user_sql\"))}function Fn(){Se({type:\"chart_modification\"})}function Yn(){at(\"feedback_buttons\"),Se({type:\"feedback_correct\"}),fT()?Pe(\"create_function\",\"GET\",{id:uE(GE)}).then(Se):console.log(\"No Question-SQL Found\")}function Vn(E,e){Pe(\"update_function\",\"POST\",{old_function_name:E,updated_function:e})}function Wn(E){Pe(\"delete_function\",\"POST\",{function_name:E}).finally(()=>{cT()})}function wn(){at(\"feedback_buttons\"),Se({type:\"feedback_correct\"});let E=fT();E&&(Pe(\"train\",\"POST\",E),Pe(\"generate_followup_questions\",\"GET\",{id:uE(GE)}).then(Se))}function wT(){at(\"feedback_buttons\"),Se({type:\"feedback_incorrect\"}),Se({type:\"user_sql\"})}function $n(E){at(\"chart_modification\"),Se({type:\"user_question\",question:\"Update the chart with these instructions: \"+E}),Pe(\"generate_plotly_figure\",\"GET\",{id:uE(GE),chart_instructions:E}).then(Se)}function xn(){var E=new WebSocket(\"ws://\"+window.location.host+\"/api/v0/log\");E.onopen=function(){console.log(\"Connected to WebSocket server at /log.\")},E.onmessage=function(e){console.log(\"Received message:\",e.data);try{var T=JSON.parse(e.data)}catch(r){console.error(\"Error parsing JSON:\",r);return}var t=document.getElementById(\"log-contents\");t&&(t.innerHTML+=\"<details> <summary>\"+T.title+\"</summary> \"+JSON.stringify(T.message)+\"</details> <br>\")},E.onclose=function(e){console.log(\"WebSocket connection closed:\",e)},E.onerror=function(e){console.error(\"WebSocket error:\",e)}}function $T(E,e,T){const t=E.slice();return t[3]=e[T],t}function xT(E){let e,T,t,r;return{c(){e=f(\"li\"),T=f(\"button\"),T.innerHTML=`<svg class=\"w-3.5 h-3.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4.26 10.147a60.436 60.436 0 00-.491 6.347A48.627 48.627 0 0112 20.904a48.627 48.627 0 018.232-4.41 60.46 60.46 0 00-.491-6.347m-15.482 0a50.57 50.57 0 00-2.658-.813A59.905 59.905 0 0112 3.493a59.902 59.902 0 0110.399 5.84c-.896.248-1.783.52-2.658.814m-15.482 0A50.697 50.697 0 0112 13.489a50.702 50.702 0 017.74-3.342M6.75 15a.75.75 0 100-1.5.75.75 0 000 1.5zm0 0v-3.675A55.378 55.378 0 0112 8.443m-7.007 11.55A5.981 5.981 0 006.75 15.75v-1.5\"></path></svg>\n              Functions`,a(T,\"class\",\"flex items-center gap-x-3 py-2 px-3 text-sm text-slate-700 rounded-md hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-slate-400 dark:hover:text-slate-300 border border-gray-200 dark:border-gray-700 w-full\")},m(R,n){V(R,e,n),l(e,T),t||(r=Ne(T,\"click\",cT),t=!0)},d(R){R&&Y(e),t=!1,r()}}}function XT(E){let e,T,t,r;return{c(){e=f(\"li\"),T=f(\"button\"),T.innerHTML=`<svg class=\"w-3.5 h-3.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4.26 10.147a60.436 60.436 0 00-.491 6.347A48.627 48.627 0 0112 20.904a48.627 48.627 0 018.232-4.41 60.46 60.46 0 00-.491-6.347m-15.482 0a50.57 50.57 0 00-2.658-.813A59.905 59.905 0 0112 3.493a59.902 59.902 0 0110.399 5.84c-.896.248-1.783.52-2.658.814m-15.482 0A50.697 50.697 0 0112 13.489a50.702 50.702 0 017.74-3.342M6.75 15a.75.75 0 100-1.5.75.75 0 000 1.5zm0 0v-3.675A55.378 55.378 0 0112 8.443m-7.007 11.55A5.981 5.981 0 006.75 15.75v-1.5\"></path></svg>\n              Training Data`,a(T,\"class\",\"flex items-center gap-x-3 py-2 px-3 text-sm text-slate-700 rounded-md hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-slate-400 dark:hover:text-slate-300 border border-gray-200 dark:border-gray-700 w-full\")},m(R,n){V(R,e,n),l(e,T),t||(r=Ne(T,\"click\",hR),t=!0)},d(R){R&&Y(e),t=!1,r()}}}function kT(E){let e,T,t,r,R,n=E[3].question+\"\",s,S,A,o;function i(){return E[2](E[3])}return{c(){e=f(\"li\"),T=f(\"button\"),t=OE(\"svg\"),r=OE(\"path\"),R=$(),s=te(n),S=$(),a(r,\"stroke-linecap\",\"round\"),a(r,\"stroke-linejoin\",\"round\"),a(r,\"d\",\"M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z\"),a(t,\"class\",\"w-3.5 h-3.5\"),a(t,\"fill\",\"none\"),a(t,\"stroke\",\"currentColor\"),a(t,\"stroke-width\",\"1.5\"),a(t,\"viewBox\",\"0 0 24 24\"),a(t,\"xmlns\",\"http://www.w3.org/2000/svg\"),a(t,\"aria-hidden\",\"true\"),a(T,\"class\",\"flex items-center text-left gap-x-3 py-2 px-3 text-sm text-slate-700 rounded-md hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-slate-400 dark:hover:text-slate-300\")},m(_,c){V(_,e,c),l(e,T),l(T,t),l(t,r),l(T,R),l(T,s),l(e,S),A||(o=Ne(T,\"click\",i),A=!0)},p(_,c){E=_,c&2&&n!==(n=E[3].question+\"\")&&Le(s,n)},d(_){_&&Y(e),A=!1,o()}}}function Xn(E){let e,T,t,r,R,n,s,S,A,o,i,_,c,P,p,C,L,I,u,H,b,M=E[0].version+\"\",O,N,D,B,h,G=E[0].function_generation&&xT(),F=E[0].show_training_data&&XT(),W=De(E[1]),x=[];for(let J=0;J<W.length;J+=1)x[J]=kT($T(E,W,J));return{c(){e=f(\"div\"),T=f(\"nav\"),t=f(\"div\"),r=f(\"img\"),n=$(),s=f(\"div\"),s.innerHTML='<button type=\"button\" class=\"w-8 h-8 inline-flex justify-center items-center gap-2 rounded-md text-gray-700 align-middle focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-white focus:ring-blue-600 transition-all dark:text-gray-400 dark:focus:ring-offset-gray-800\" data-hs-overlay=\"#application-sidebar\" aria-controls=\"application-sidebar\" aria-label=\"Toggle navigation\"><svg class=\"w-4 h-4\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\"><path d=\"M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854Z\"></path></svg> <span class=\"sr-only\">Sidebar</span></button>',S=$(),A=f(\"div\"),o=f(\"ul\"),G&&G.c(),i=$(),F&&F.c(),_=$(),c=f(\"li\"),P=f(\"button\"),P.innerHTML=`<svg class=\"w-3.5 h-3.5\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M8 2C8.47339 2 8.85714 2.38376 8.85714 2.85714V7.14286L13.1429 7.14286C13.6162 7.14286 14 7.52661 14 8C14 8.47339 13.6162 8.85714 13.1429 8.85714L8.85714 8.85715V13.1429C8.85714 13.6162 8.47339 14 8 14C7.52661 14 7.14286 13.6162 7.14286 13.1429V8.85715L2.85714 8.85715C2.38376 8.85715 2 8.4734 2 8.00001C2 7.52662 2.38376 7.14287 2.85714 7.14287L7.14286 7.14286V2.85714C7.14286 2.38376 7.52661 2 8 2Z\" fill=\"currentColor\"></path></svg>\n              New question`,p=$();for(let J=0;J<x.length;J+=1)x[J].c();C=$(),L=f(\"div\"),I=f(\"div\"),u=f(\"p\"),H=f(\"span\"),b=te(`\n            v`),O=te(M),N=$(),D=f(\"div\"),D.innerHTML=`<a class=\"flex justify-between items-center gap-x-3 py-2 px-3 text-sm text-slate-700 rounded-md hover:bg-gray-100 dark:hover:bg-gray-900 dark:text-slate-400 dark:hover:text-slate-300\" href=\"/auth/logout\">Sign out\n            <svg class=\"w-3.5 h-3.5\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M10 3.5a.5.5 0 0 0-.5-.5h-8a.5.5 0 0 0-.5.5v9a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-2a.5.5 0 0 1 1 0v2A1.5 1.5 0 0 1 9.5 14h-8A1.5 1.5 0 0 1 0 12.5v-9A1.5 1.5 0 0 1 1.5 2h8A1.5 1.5 0 0 1 11 3.5v2a.5.5 0 0 1-1 0v-2z\"></path><path fill-rule=\"evenodd\" d=\"M4.146 8.354a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L5.707 7.5H14.5a.5.5 0 0 1 0 1H5.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3z\"></path></svg></a>`,a(r,\"class\",\"w-28 h-auto\"),VT(r.src,R=E[0].logo)||a(r,\"src\",R),a(r,\"alt\",\"Vanna Logo\"),a(s,\"class\",\"lg:hidden\"),a(t,\"class\",\"flex items-center justify-between py-4 pr-4 pl-7\"),a(P,\"class\",\"w-full py-2 px-4 inline-flex items-center gap-x-2 text-sm font-semibold rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none\"),a(o,\"class\",\"space-y-1.5 p-4\"),a(A,\"class\",\"h-full\"),a(H,\"class\",\"block w-1.5 h-1.5 rounded-full bg-green-600\"),a(u,\"class\",\"inline-flex items-center gap-x-2 text-xs text-green-600\"),a(I,\"class\",\"py-2.5 px-7\"),a(D,\"class\",\"p-4 border-t border-gray-200 dark:border-gray-700\"),a(L,\"class\",\"mt-auto\"),a(T,\"class\",\"hs-accordion-group w-full h-full flex flex-col\"),a(T,\"data-hs-accordion-always-open\",\"\"),a(e,\"id\",\"application-sidebar\"),a(e,\"class\",\"hs-overlay hs-overlay-open:translate-x-0 -translate-x-full transition-all duration-300 transform hidden fixed top-0 left-0 bottom-0 z-[60] w-64 bg-white border-r border-gray-200 overflow-y-auto scrollbar-y lg:block lg:translate-x-0 lg:right-auto lg:bottom-0 dark:scrollbar-y dark:bg-slate-900 dark:border-gray-700\")},m(J,oe){V(J,e,oe),l(e,T),l(T,t),l(t,r),l(t,n),l(t,s),l(T,S),l(T,A),l(A,o),G&&G.m(o,null),l(o,i),F&&F.m(o,null),l(o,_),l(o,c),l(c,P),l(o,p);for(let z=0;z<x.length;z+=1)x[z]&&x[z].m(o,null);l(T,C),l(T,L),l(L,I),l(I,u),l(u,H),l(u,b),l(u,O),l(L,N),l(L,D),B||(h=Ne(P,\"click\",it),B=!0)},p(J,[oe]){if(oe&1&&!VT(r.src,R=J[0].logo)&&a(r,\"src\",R),J[0].function_generation?G||(G=xT(),G.c(),G.m(o,i)):G&&(G.d(1),G=null),J[0].show_training_data?F||(F=XT(),F.c(),F.m(o,_)):F&&(F.d(1),F=null),oe&2){W=De(J[1]);let z;for(z=0;z<W.length;z+=1){const Oe=$T(J,W,z);x[z]?x[z].p(Oe,oe):(x[z]=kT(Oe),x[z].c(),x[z].m(o,null))}for(;z<x.length;z+=1)x[z].d(1);x.length=W.length}oe&1&&M!==(M=J[0].version+\"\")&&Le(O,M)},i:j,o:j,d(J){J&&Y(e),G&&G.d(),F&&F.d(),nE(x,J),B=!1,h()}}}function kn(E,e,T){let t,r;return eE(E,VE,n=>T(0,t=n)),eE(E,CT,n=>T(1,r=n)),[t,r,n=>{Mn(n.id)}]}class Kn extends ue{constructor(e){super(),Ce(this,e,kn,Xn,_e,{})}}var Jn=typeof globalThis<\"u\"?globalThis:typeof window<\"u\"?window:typeof global<\"u\"?global:typeof self<\"u\"?self:{};function qn(E){return E&&E.__esModule&&Object.prototype.hasOwnProperty.call(E,\"default\")?E.default:E}var Qn={exports:{}};/*! For license information please see preline.js.LICENSE.txt */(function(E,e){(function(T,t){E.exports=t()})(self,function(){return(()=>{var T={661:(n,s,S)=>{function A(p){return A=typeof Symbol==\"function\"&&typeof Symbol.iterator==\"symbol\"?function(C){return typeof C}:function(C){return C&&typeof Symbol==\"function\"&&C.constructor===Symbol&&C!==Symbol.prototype?\"symbol\":typeof C},A(p)}function o(p,C){for(var L=0;L<C.length;L++){var I=C[L];I.enumerable=I.enumerable||!1,I.configurable=!0,\"value\"in I&&(I.writable=!0),Object.defineProperty(p,I.key,I)}}function i(p,C){return i=Object.setPrototypeOf||function(L,I){return L.__proto__=I,L},i(p,C)}function _(p,C){if(C&&(A(C)===\"object\"||typeof C==\"function\"))return C;if(C!==void 0)throw new TypeError(\"Derived constructors may only return object or undefined\");return function(L){if(L===void 0)throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");return L}(p)}function c(p){return c=Object.setPrototypeOf?Object.getPrototypeOf:function(C){return C.__proto__||Object.getPrototypeOf(C)},c(p)}var P=function(p){(function(M,O){if(typeof O!=\"function\"&&O!==null)throw new TypeError(\"Super expression must either be null or a function\");M.prototype=Object.create(O&&O.prototype,{constructor:{value:M,writable:!0,configurable:!0}}),Object.defineProperty(M,\"prototype\",{writable:!1}),O&&i(M,O)})(b,p);var C,L,I,u,H=(I=b,u=function(){if(typeof Reflect>\"u\"||!Reflect.construct||Reflect.construct.sham)return!1;if(typeof Proxy==\"function\")return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){})),!0}catch{return!1}}(),function(){var M,O=c(I);if(u){var N=c(this).constructor;M=Reflect.construct(O,arguments,N)}else M=O.apply(this,arguments);return _(this,M)});function b(){return function(M,O){if(!(M instanceof O))throw new TypeError(\"Cannot call a class as a function\")}(this,b),H.call(this,\".hs-accordion\")}return C=b,(L=[{key:\"init\",value:function(){var M=this;document.addEventListener(\"click\",function(O){var N=O.target,D=N.closest(M.selector),B=N.closest(\".hs-accordion-toggle\"),h=N.closest(\".hs-accordion-group\");D&&h&&B&&(M._hideAll(D),M.show(D))})}},{key:\"show\",value:function(M){var O=this;if(M.classList.contains(\"active\"))return this.hide(M);M.classList.add(\"active\");var N=M.querySelector(\".hs-accordion-content\");N.style.display=\"block\",N.style.height=0,setTimeout(function(){N.style.height=\"\".concat(N.scrollHeight,\"px\")}),this.afterTransition(N,function(){M.classList.contains(\"active\")&&(N.style.height=\"\",O._fireEvent(\"open\",M),O._dispatch(\"open.hs.accordion\",M,M))})}},{key:\"hide\",value:function(M){var O=this,N=M.querySelector(\".hs-accordion-content\");N.style.height=\"\".concat(N.scrollHeight,\"px\"),setTimeout(function(){N.style.height=0}),this.afterTransition(N,function(){M.classList.contains(\"active\")||(N.style.display=\"\",O._fireEvent(\"hide\",M),O._dispatch(\"hide.hs.accordion\",M,M))}),M.classList.remove(\"active\")}},{key:\"_hideAll\",value:function(M){var O=this,N=M.closest(\".hs-accordion-group\");N.hasAttribute(\"data-hs-accordion-always-open\")||N.querySelectorAll(this.selector).forEach(function(D){M!==D&&O.hide(D)})}}])&&o(C.prototype,L),Object.defineProperty(C,\"prototype\",{writable:!1}),b}(S(765).Z);window.HSAccordion=new P,document.addEventListener(\"load\",window.HSAccordion.init())},795:(n,s,S)=>{function A(C){return A=typeof Symbol==\"function\"&&typeof Symbol.iterator==\"symbol\"?function(L){return typeof L}:function(L){return L&&typeof Symbol==\"function\"&&L.constructor===Symbol&&L!==Symbol.prototype?\"symbol\":typeof L},A(C)}function o(C,L){(L==null||L>C.length)&&(L=C.length);for(var I=0,u=new Array(L);I<L;I++)u[I]=C[I];return u}function i(C,L){for(var I=0;I<L.length;I++){var u=L[I];u.enumerable=u.enumerable||!1,u.configurable=!0,\"value\"in u&&(u.writable=!0),Object.defineProperty(C,u.key,u)}}function _(C,L){return _=Object.setPrototypeOf||function(I,u){return I.__proto__=u,I},_(C,L)}function c(C,L){if(L&&(A(L)===\"object\"||typeof L==\"function\"))return L;if(L!==void 0)throw new TypeError(\"Derived constructors may only return object or undefined\");return function(I){if(I===void 0)throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");return I}(C)}function P(C){return P=Object.setPrototypeOf?Object.getPrototypeOf:function(L){return L.__proto__||Object.getPrototypeOf(L)},P(C)}var p=function(C){(function(O,N){if(typeof N!=\"function\"&&N!==null)throw new TypeError(\"Super expression must either be null or a function\");O.prototype=Object.create(N&&N.prototype,{constructor:{value:O,writable:!0,configurable:!0}}),Object.defineProperty(O,\"prototype\",{writable:!1}),N&&_(O,N)})(M,C);var L,I,u,H,b=(u=M,H=function(){if(typeof Reflect>\"u\"||!Reflect.construct||Reflect.construct.sham)return!1;if(typeof Proxy==\"function\")return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){})),!0}catch{return!1}}(),function(){var O,N=P(u);if(H){var D=P(this).constructor;O=Reflect.construct(N,arguments,D)}else O=N.apply(this,arguments);return c(this,O)});function M(){return function(O,N){if(!(O instanceof N))throw new TypeError(\"Cannot call a class as a function\")}(this,M),b.call(this,\"[data-hs-collapse]\")}return L=M,(I=[{key:\"init\",value:function(){var O=this;document.addEventListener(\"click\",function(N){var D=N.target.closest(O.selector);if(D){var B=document.querySelectorAll(D.getAttribute(\"data-hs-collapse\"));O.toggle(B)}})}},{key:\"toggle\",value:function(O){var N,D=this;O.length&&(N=O,function(B){if(Array.isArray(B))return o(B)}(N)||function(B){if(typeof Symbol<\"u\"&&B[Symbol.iterator]!=null||B[\"@@iterator\"]!=null)return Array.from(B)}(N)||function(B,h){if(B){if(typeof B==\"string\")return o(B,h);var G=Object.prototype.toString.call(B).slice(8,-1);return G===\"Object\"&&B.constructor&&(G=B.constructor.name),G===\"Map\"||G===\"Set\"?Array.from(B):G===\"Arguments\"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(G)?o(B,h):void 0}}(N)||function(){throw new TypeError(`Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}()).forEach(function(B){B.classList.contains(\"hidden\")?D.show(B):D.hide(B)})}},{key:\"show\",value:function(O){var N=this;O.classList.add(\"open\"),O.classList.remove(\"hidden\"),O.style.height=0,document.querySelectorAll(this.selector).forEach(function(D){O.closest(D.getAttribute(\"data-hs-collapse\"))&&D.classList.add(\"open\")}),O.style.height=\"\".concat(O.scrollHeight,\"px\"),this.afterTransition(O,function(){O.classList.contains(\"open\")&&(O.style.height=\"\",N._fireEvent(\"open\",O),N._dispatch(\"open.hs.collapse\",O,O))})}},{key:\"hide\",value:function(O){var N=this;O.style.height=\"\".concat(O.scrollHeight,\"px\"),setTimeout(function(){O.style.height=0}),O.classList.remove(\"open\"),this.afterTransition(O,function(){O.classList.contains(\"open\")||(O.classList.add(\"hidden\"),O.style.height=null,N._fireEvent(\"hide\",O),N._dispatch(\"hide.hs.collapse\",O,O),O.querySelectorAll(\".hs-mega-menu-content.block\").forEach(function(D){D.classList.remove(\"block\"),D.classList.add(\"hidden\")}))}),document.querySelectorAll(this.selector).forEach(function(D){O.closest(D.getAttribute(\"data-hs-collapse\"))&&D.classList.remove(\"open\")})}}])&&i(L.prototype,I),Object.defineProperty(L,\"prototype\",{writable:!1}),M}(S(765).Z);window.HSCollapse=new p,document.addEventListener(\"load\",window.HSCollapse.init())},682:(n,s,S)=>{var A=S(714),o=S(765);const i={historyIndex:-1,addHistory:function(H){this.historyIndex=H},existsInHistory:function(H){return H>this.historyIndex},clearHistory:function(){this.historyIndex=-1}};function _(H){return _=typeof Symbol==\"function\"&&typeof Symbol.iterator==\"symbol\"?function(b){return typeof b}:function(b){return b&&typeof Symbol==\"function\"&&b.constructor===Symbol&&b!==Symbol.prototype?\"symbol\":typeof b},_(H)}function c(H){return function(b){if(Array.isArray(b))return P(b)}(H)||function(b){if(typeof Symbol<\"u\"&&b[Symbol.iterator]!=null||b[\"@@iterator\"]!=null)return Array.from(b)}(H)||function(b,M){if(b){if(typeof b==\"string\")return P(b,M);var O=Object.prototype.toString.call(b).slice(8,-1);return O===\"Object\"&&b.constructor&&(O=b.constructor.name),O===\"Map\"||O===\"Set\"?Array.from(b):O===\"Arguments\"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(O)?P(b,M):void 0}}(H)||function(){throw new TypeError(`Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}()}function P(H,b){(b==null||b>H.length)&&(b=H.length);for(var M=0,O=new Array(b);M<b;M++)O[M]=H[M];return O}function p(H,b){for(var M=0;M<b.length;M++){var O=b[M];O.enumerable=O.enumerable||!1,O.configurable=!0,\"value\"in O&&(O.writable=!0),Object.defineProperty(H,O.key,O)}}function C(H,b){return C=Object.setPrototypeOf||function(M,O){return M.__proto__=O,M},C(H,b)}function L(H,b){if(b&&(_(b)===\"object\"||typeof b==\"function\"))return b;if(b!==void 0)throw new TypeError(\"Derived constructors may only return object or undefined\");return function(M){if(M===void 0)throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");return M}(H)}function I(H){return I=Object.setPrototypeOf?Object.getPrototypeOf:function(b){return b.__proto__||Object.getPrototypeOf(b)},I(H)}var u=function(H){(function(h,G){if(typeof G!=\"function\"&&G!==null)throw new TypeError(\"Super expression must either be null or a function\");h.prototype=Object.create(G&&G.prototype,{constructor:{value:h,writable:!0,configurable:!0}}),Object.defineProperty(h,\"prototype\",{writable:!1}),G&&C(h,G)})(B,H);var b,M,O,N,D=(O=B,N=function(){if(typeof Reflect>\"u\"||!Reflect.construct||Reflect.construct.sham)return!1;if(typeof Proxy==\"function\")return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){})),!0}catch{return!1}}(),function(){var h,G=I(O);if(N){var F=I(this).constructor;h=Reflect.construct(G,arguments,F)}else h=G.apply(this,arguments);return L(this,h)});function B(){var h;return function(G,F){if(!(G instanceof F))throw new TypeError(\"Cannot call a class as a function\")}(this,B),(h=D.call(this,\".hs-dropdown\")).positions={top:\"top\",\"top-left\":\"top-start\",\"top-right\":\"top-end\",bottom:\"bottom\",\"bottom-left\":\"bottom-start\",\"bottom-right\":\"bottom-end\",right:\"right\",\"right-top\":\"right-start\",\"right-bottom\":\"right-end\",left:\"left\",\"left-top\":\"left-start\",\"left-bottom\":\"left-end\"},h.absoluteStrategyModifiers=function(G){return[{name:\"applyStyles\",fn:function(F){var W=(window.getComputedStyle(G).getPropertyValue(\"--strategy\")||\"absolute\").replace(\" \",\"\"),x=(window.getComputedStyle(G).getPropertyValue(\"--adaptive\")||\"adaptive\").replace(\" \",\"\");F.state.elements.popper.style.position=W,F.state.elements.popper.style.transform=x===\"adaptive\"?F.state.styles.popper.transform:null,F.state.elements.popper.style.top=null,F.state.elements.popper.style.bottom=null,F.state.elements.popper.style.left=null,F.state.elements.popper.style.right=null,F.state.elements.popper.style.margin=0}},{name:\"computeStyles\",options:{adaptive:!1}}]},h._history=i,h}return b=B,M=[{key:\"init\",value:function(){var h=this;document.addEventListener(\"click\",function(G){var F=G.target,W=F.closest(h.selector),x=F.closest(\".hs-dropdown-menu\");if(W&&W.classList.contains(\"open\")||h._closeOthers(W),x){var J=(window.getComputedStyle(W).getPropertyValue(\"--auto-close\")||\"\").replace(\" \",\"\");if((J==\"false\"||J==\"inside\")&&!W.parentElement.closest(h.selector))return}W&&(W.classList.contains(\"open\")?h.close(W):h.open(W))}),document.addEventListener(\"mousemove\",function(G){var F=G.target,W=F.closest(h.selector);if(F.closest(\".hs-dropdown-menu\"),W){var x=(window.getComputedStyle(W).getPropertyValue(\"--trigger\")||\"click\").replace(\" \",\"\");if(x!==\"hover\")return;W&&W.classList.contains(\"open\")||h._closeOthers(W),x!==\"hover\"||W.classList.contains(\"open\")||/iPad|iPhone|iPod/.test(navigator.platform)||navigator.maxTouchPoints&&navigator.maxTouchPoints>2&&/MacIntel/.test(navigator.platform)||navigator.maxTouchPoints&&navigator.maxTouchPoints>2&&/MacIntel/.test(navigator.platform)||h._hover(F)}}),document.addEventListener(\"keydown\",this._keyboardSupport.bind(this)),window.addEventListener(\"resize\",function(){document.querySelectorAll(\".hs-dropdown.open\").forEach(function(G){h.close(G,!0)})})}},{key:\"_closeOthers\",value:function(){var h=this,G=arguments.length>0&&arguments[0]!==void 0?arguments[0]:null,F=document.querySelectorAll(\"\".concat(this.selector,\".open\"));F.forEach(function(W){if(!G||G.closest(\".hs-dropdown.open\")!==W){var x=(window.getComputedStyle(W).getPropertyValue(\"--auto-close\")||\"\").replace(\" \",\"\");x!=\"false\"&&x!=\"outside\"&&h.close(W)}})}},{key:\"_hover\",value:function(h){var G=this,F=h.closest(this.selector);this.open(F),document.addEventListener(\"mousemove\",function W(x){x.target.closest(G.selector)&&x.target.closest(G.selector)!==F.parentElement.closest(G.selector)||(G.close(F),document.removeEventListener(\"mousemove\",W,!0))},!0)}},{key:\"close\",value:function(h){var G=this,F=arguments.length>1&&arguments[1]!==void 0&&arguments[1],W=h.querySelector(\".hs-dropdown-menu\"),x=function(){h.classList.contains(\"open\")||(W.classList.remove(\"block\"),W.classList.add(\"hidden\"),W.style.inset=null,W.style.position=null,h._popper&&h._popper.destroy())};F||this.afterTransition(h.querySelector(\"[data-hs-dropdown-transition]\")||W,function(){x()}),W.style.margin=null,h.classList.remove(\"open\"),F&&x(),this._fireEvent(\"close\",h),this._dispatch(\"close.hs.dropdown\",h,h);var J=W.querySelectorAll(\".hs-dropdown.open\");J.forEach(function(oe){G.close(oe,!0)})}},{key:\"open\",value:function(h){var G=h.querySelector(\".hs-dropdown-menu\"),F=(window.getComputedStyle(h).getPropertyValue(\"--placement\")||\"\").replace(\" \",\"\"),W=(window.getComputedStyle(h).getPropertyValue(\"--strategy\")||\"fixed\").replace(\" \",\"\"),x=((window.getComputedStyle(h).getPropertyValue(\"--adaptive\")||\"adaptive\").replace(\" \",\"\"),parseInt((window.getComputedStyle(h).getPropertyValue(\"--offset\")||\"10\").replace(\" \",\"\")));if(W!==\"static\"){h._popper&&h._popper.destroy();var J=(0,A.fi)(h,G,{placement:this.positions[F]||\"bottom-start\",strategy:W,modifiers:[].concat(c(W!==\"fixed\"?this.absoluteStrategyModifiers(h):[]),[{name:\"offset\",options:{offset:[0,x]}}])});h._popper=J}G.style.margin=null,G.classList.add(\"block\"),G.classList.remove(\"hidden\"),setTimeout(function(){h.classList.add(\"open\")}),this._fireEvent(\"open\",h),this._dispatch(\"open.hs.dropdown\",h,h)}},{key:\"_keyboardSupport\",value:function(h){var G=document.querySelector(\".hs-dropdown.open\");if(G)return h.keyCode===27?(h.preventDefault(),this._esc(G)):h.keyCode===40?(h.preventDefault(),this._down(G)):h.keyCode===38?(h.preventDefault(),this._up(G)):h.keyCode===36?(h.preventDefault(),this._start(G)):h.keyCode===35?(h.preventDefault(),this._end(G)):void this._byChar(G,h.key)}},{key:\"_esc\",value:function(h){this.close(h)}},{key:\"_up\",value:function(h){var G=h.querySelector(\".hs-dropdown-menu\"),F=c(G.querySelectorAll(\"a\")).reverse().filter(function(J){return!J.disabled}),W=G.querySelector(\"a:focus\"),x=F.findIndex(function(J){return J===W});x+1<F.length&&x++,F[x].focus()}},{key:\"_down\",value:function(h){var G=h.querySelector(\".hs-dropdown-menu\"),F=c(G.querySelectorAll(\"a\")).filter(function(J){return!J.disabled}),W=G.querySelector(\"a:focus\"),x=F.findIndex(function(J){return J===W});x+1<F.length&&x++,F[x].focus()}},{key:\"_start\",value:function(h){var G=c(h.querySelector(\".hs-dropdown-menu\").querySelectorAll(\"a\")).filter(function(F){return!F.disabled});G.length&&G[0].focus()}},{key:\"_end\",value:function(h){var G=c(h.querySelector(\".hs-dropdown-menu\").querySelectorAll(\"a\")).reverse().filter(function(F){return!F.disabled});G.length&&G[0].focus()}},{key:\"_byChar\",value:function(h,G){var F=this,W=c(h.querySelector(\".hs-dropdown-menu\").querySelectorAll(\"a\")),x=function(){return W.findIndex(function(oe,z){return oe.innerText.toLowerCase().charAt(0)===G.toLowerCase()&&F._history.existsInHistory(z)})},J=x();J===-1&&(this._history.clearHistory(),J=x()),J!==-1&&(W[J].focus(),this._history.addHistory(J))}},{key:\"toggle\",value:function(h){h.classList.contains(\"open\")?this.close(h):this.open(h)}}],M&&p(b.prototype,M),Object.defineProperty(b,\"prototype\",{writable:!1}),B}(o.Z);window.HSDropdown=new u,document.addEventListener(\"load\",window.HSDropdown.init())},284:(n,s,S)=>{function A(C){return A=typeof Symbol==\"function\"&&typeof Symbol.iterator==\"symbol\"?function(L){return typeof L}:function(L){return L&&typeof Symbol==\"function\"&&L.constructor===Symbol&&L!==Symbol.prototype?\"symbol\":typeof L},A(C)}function o(C,L){(L==null||L>C.length)&&(L=C.length);for(var I=0,u=new Array(L);I<L;I++)u[I]=C[I];return u}function i(C,L){for(var I=0;I<L.length;I++){var u=L[I];u.enumerable=u.enumerable||!1,u.configurable=!0,\"value\"in u&&(u.writable=!0),Object.defineProperty(C,u.key,u)}}function _(C,L){return _=Object.setPrototypeOf||function(I,u){return I.__proto__=u,I},_(C,L)}function c(C,L){if(L&&(A(L)===\"object\"||typeof L==\"function\"))return L;if(L!==void 0)throw new TypeError(\"Derived constructors may only return object or undefined\");return function(I){if(I===void 0)throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");return I}(C)}function P(C){return P=Object.setPrototypeOf?Object.getPrototypeOf:function(L){return L.__proto__||Object.getPrototypeOf(L)},P(C)}var p=function(C){(function(O,N){if(typeof N!=\"function\"&&N!==null)throw new TypeError(\"Super expression must either be null or a function\");O.prototype=Object.create(N&&N.prototype,{constructor:{value:O,writable:!0,configurable:!0}}),Object.defineProperty(O,\"prototype\",{writable:!1}),N&&_(O,N)})(M,C);var L,I,u,H,b=(u=M,H=function(){if(typeof Reflect>\"u\"||!Reflect.construct||Reflect.construct.sham)return!1;if(typeof Proxy==\"function\")return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){})),!0}catch{return!1}}(),function(){var O,N=P(u);if(H){var D=P(this).constructor;O=Reflect.construct(N,arguments,D)}else O=N.apply(this,arguments);return c(this,O)});function M(){var O;return function(N,D){if(!(N instanceof D))throw new TypeError(\"Cannot call a class as a function\")}(this,M),(O=b.call(this,\"[data-hs-overlay]\")).openNextOverlay=!1,O}return L=M,(I=[{key:\"init\",value:function(){var O=this;document.addEventListener(\"click\",function(N){var D=N.target.closest(O.selector),B=N.target.closest(\"[data-hs-overlay-close]\"),h=N.target.getAttribute(\"aria-overlay\")===\"true\";return B?O.close(B.closest(\".hs-overlay.open\")):D?O.toggle(document.querySelector(D.getAttribute(\"data-hs-overlay\"))):void(h&&O._onBackdropClick(N.target))}),document.addEventListener(\"keydown\",function(N){if(N.keyCode===27){var D=document.querySelector(\".hs-overlay.open\");if(!D)return;setTimeout(function(){D.getAttribute(\"data-hs-overlay-keyboard\")!==\"false\"&&O.close(D)})}})}},{key:\"toggle\",value:function(O){O&&(O.classList.contains(\"hidden\")?this.open(O):this.close(O))}},{key:\"open\",value:function(O){var N=this;if(O){var D=document.querySelector(\".hs-overlay.open\"),B=this.getClassProperty(O,\"--body-scroll\",\"false\")!==\"true\";if(D)return this.openNextOverlay=!0,this.close(D).then(function(){N.open(O),N.openNextOverlay=!1});B&&(document.body.style.overflow=\"hidden\"),this._buildBackdrop(O),this._checkTimer(O),this._autoHide(O),O.classList.remove(\"hidden\"),O.setAttribute(\"aria-overlay\",\"true\"),O.setAttribute(\"tabindex\",\"-1\"),setTimeout(function(){O.classList.contains(\"hidden\")||(O.classList.add(\"open\"),N._fireEvent(\"open\",O),N._dispatch(\"open.hs.overlay\",O,O),N._focusInput(O))},50)}}},{key:\"close\",value:function(O){var N=this;return new Promise(function(D){O&&(O.classList.remove(\"open\"),O.removeAttribute(\"aria-overlay\"),O.removeAttribute(\"tabindex\",\"-1\"),N.afterTransition(O,function(){O.classList.contains(\"open\")||(O.classList.add(\"hidden\"),N._destroyBackdrop(),N._fireEvent(\"close\",O),N._dispatch(\"close.hs.overlay\",O,O),document.body.style.overflow=\"\",D(O))}))})}},{key:\"_autoHide\",value:function(O){var N=this,D=parseInt(this.getClassProperty(O,\"--auto-hide\",\"0\"));D&&(O.autoHide=setTimeout(function(){N.close(O)},D))}},{key:\"_checkTimer\",value:function(O){O.autoHide&&(clearTimeout(O.autoHide),delete O.autoHide)}},{key:\"_onBackdropClick\",value:function(O){this.getClassProperty(O,\"--overlay-backdrop\",\"true\")!==\"static\"&&this.close(O)}},{key:\"_buildBackdrop\",value:function(O){var N,D=this,B=O.getAttribute(\"data-hs-overlay-backdrop-container\")||!1,h=document.createElement(\"div\"),G=\"transition duration fixed inset-0 z-50 bg-gray-900 bg-opacity-50 dark:bg-opacity-80 hs-overlay-backdrop\",F=function(J,oe){var z=typeof Symbol<\"u\"&&J[Symbol.iterator]||J[\"@@iterator\"];if(!z){if(Array.isArray(J)||(z=function(me,fE){if(me){if(typeof me==\"string\")return o(me,fE);var rE=Object.prototype.toString.call(me).slice(8,-1);return rE===\"Object\"&&me.constructor&&(rE=me.constructor.name),rE===\"Map\"||rE===\"Set\"?Array.from(me):rE===\"Arguments\"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(rE)?o(me,fE):void 0}}(J))||oe&&J&&typeof J.length==\"number\"){z&&(J=z);var Oe=0,Ue=function(){};return{s:Ue,n:function(){return Oe>=J.length?{done:!0}:{done:!1,value:J[Oe++]}},e:function(me){throw me},f:Ue}}throw new TypeError(`Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}var ye,TE=!0,dE=!1;return{s:function(){z=z.call(J)},n:function(){var me=z.next();return TE=me.done,me},e:function(me){dE=!0,ye=me},f:function(){try{TE||z.return==null||z.return()}finally{if(dE)throw ye}}}}(O.classList.values());try{for(F.s();!(N=F.n()).done;){var W=N.value;W.startsWith(\"hs-overlay-backdrop-open:\")&&(G+=\" \".concat(W))}}catch(J){F.e(J)}finally{F.f()}var x=this.getClassProperty(O,\"--overlay-backdrop\",\"true\")!==\"static\";this.getClassProperty(O,\"--overlay-backdrop\",\"true\")===\"false\"||(B&&((h=document.querySelector(B).cloneNode(!0)).classList.remove(\"hidden\"),G=h.classList,h.classList=\"\"),x&&h.addEventListener(\"click\",function(){return D.close(O)},!0),h.setAttribute(\"data-hs-overlay-backdrop-template\",\"\"),document.body.appendChild(h),setTimeout(function(){h.classList=G}))}},{key:\"_destroyBackdrop\",value:function(){var O=document.querySelector(\"[data-hs-overlay-backdrop-template]\");O&&(this.openNextOverlay&&(O.style.transitionDuration=\"\".concat(1.8*parseFloat(window.getComputedStyle(O).transitionDuration.replace(/[^\\\\d.-]/g,\"\")),\"s\")),O.classList.add(\"opacity-0\"),this.afterTransition(O,function(){O.remove()}))}},{key:\"_focusInput\",value:function(O){var N=O.querySelector(\"[autofocus]\");N&&N.focus()}}])&&i(L.prototype,I),Object.defineProperty(L,\"prototype\",{writable:!1}),M}(S(765).Z);window.HSOverlay=new p,document.addEventListener(\"load\",window.HSOverlay.init())},181:(n,s,S)=>{function A(p){return A=typeof Symbol==\"function\"&&typeof Symbol.iterator==\"symbol\"?function(C){return typeof C}:function(C){return C&&typeof Symbol==\"function\"&&C.constructor===Symbol&&C!==Symbol.prototype?\"symbol\":typeof C},A(p)}function o(p,C){for(var L=0;L<C.length;L++){var I=C[L];I.enumerable=I.enumerable||!1,I.configurable=!0,\"value\"in I&&(I.writable=!0),Object.defineProperty(p,I.key,I)}}function i(p,C){return i=Object.setPrototypeOf||function(L,I){return L.__proto__=I,L},i(p,C)}function _(p,C){if(C&&(A(C)===\"object\"||typeof C==\"function\"))return C;if(C!==void 0)throw new TypeError(\"Derived constructors may only return object or undefined\");return function(L){if(L===void 0)throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");return L}(p)}function c(p){return c=Object.setPrototypeOf?Object.getPrototypeOf:function(C){return C.__proto__||Object.getPrototypeOf(C)},c(p)}var P=function(p){(function(M,O){if(typeof O!=\"function\"&&O!==null)throw new TypeError(\"Super expression must either be null or a function\");M.prototype=Object.create(O&&O.prototype,{constructor:{value:M,writable:!0,configurable:!0}}),Object.defineProperty(M,\"prototype\",{writable:!1}),O&&i(M,O)})(b,p);var C,L,I,u,H=(I=b,u=function(){if(typeof Reflect>\"u\"||!Reflect.construct||Reflect.construct.sham)return!1;if(typeof Proxy==\"function\")return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){})),!0}catch{return!1}}(),function(){var M,O=c(I);if(u){var N=c(this).constructor;M=Reflect.construct(O,arguments,N)}else M=O.apply(this,arguments);return _(this,M)});function b(){return function(M,O){if(!(M instanceof O))throw new TypeError(\"Cannot call a class as a function\")}(this,b),H.call(this,\"[data-hs-remove-element]\")}return C=b,(L=[{key:\"init\",value:function(){var M=this;document.addEventListener(\"click\",function(O){var N=O.target.closest(M.selector);if(N){var D=document.querySelector(N.getAttribute(\"data-hs-remove-element\"));D&&(D.classList.add(\"hs-removing\"),M.afterTransition(D,function(){D.remove()}))}})}}])&&o(C.prototype,L),Object.defineProperty(C,\"prototype\",{writable:!1}),b}(S(765).Z);window.HSRemoveElement=new P,document.addEventListener(\"load\",window.HSRemoveElement.init())},778:(n,s,S)=>{function A(p){return A=typeof Symbol==\"function\"&&typeof Symbol.iterator==\"symbol\"?function(C){return typeof C}:function(C){return C&&typeof Symbol==\"function\"&&C.constructor===Symbol&&C!==Symbol.prototype?\"symbol\":typeof C},A(p)}function o(p,C){for(var L=0;L<C.length;L++){var I=C[L];I.enumerable=I.enumerable||!1,I.configurable=!0,\"value\"in I&&(I.writable=!0),Object.defineProperty(p,I.key,I)}}function i(p,C){return i=Object.setPrototypeOf||function(L,I){return L.__proto__=I,L},i(p,C)}function _(p,C){if(C&&(A(C)===\"object\"||typeof C==\"function\"))return C;if(C!==void 0)throw new TypeError(\"Derived constructors may only return object or undefined\");return function(L){if(L===void 0)throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");return L}(p)}function c(p){return c=Object.setPrototypeOf?Object.getPrototypeOf:function(C){return C.__proto__||Object.getPrototypeOf(C)},c(p)}var P=function(p){(function(M,O){if(typeof O!=\"function\"&&O!==null)throw new TypeError(\"Super expression must either be null or a function\");M.prototype=Object.create(O&&O.prototype,{constructor:{value:M,writable:!0,configurable:!0}}),Object.defineProperty(M,\"prototype\",{writable:!1}),O&&i(M,O)})(b,p);var C,L,I,u,H=(I=b,u=function(){if(typeof Reflect>\"u\"||!Reflect.construct||Reflect.construct.sham)return!1;if(typeof Proxy==\"function\")return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){})),!0}catch{return!1}}(),function(){var M,O=c(I);if(u){var N=c(this).constructor;M=Reflect.construct(O,arguments,N)}else M=O.apply(this,arguments);return _(this,M)});function b(){var M;return function(O,N){if(!(O instanceof N))throw new TypeError(\"Cannot call a class as a function\")}(this,b),(M=H.call(this,\"[data-hs-scrollspy] \")).activeSection=null,M}return C=b,(L=[{key:\"init\",value:function(){var M=this;document.querySelectorAll(this.selector).forEach(function(O){var N=document.querySelector(O.getAttribute(\"data-hs-scrollspy\")),D=O.querySelectorAll(\"[href]\"),B=N.children,h=O.getAttribute(\"data-hs-scrollspy-scrollable-parent\")?document.querySelector(O.getAttribute(\"data-hs-scrollspy-scrollable-parent\")):document;Array.from(B).forEach(function(G){G.getAttribute(\"id\")&&h.addEventListener(\"scroll\",function(F){return M._update({$scrollspyEl:O,$scrollspyContentEl:N,links:D,$sectionEl:G,sections:B,ev:F})})}),D.forEach(function(G){G.addEventListener(\"click\",function(F){F.preventDefault(),G.getAttribute(\"href\")!==\"javascript:;\"&&M._scrollTo({$scrollspyEl:O,$scrollableEl:h,$link:G})})})})}},{key:\"_update\",value:function(M){var O=M.ev,N=M.$scrollspyEl,D=(M.sections,M.links),B=M.$sectionEl,h=parseInt(this.getClassProperty(N,\"--scrollspy-offset\",\"0\")),G=this.getClassProperty(B,\"--scrollspy-offset\")||h,F=O.target===document?0:parseInt(O.target.getBoundingClientRect().top),W=parseInt(B.getBoundingClientRect().top)-G-F,x=B.offsetHeight;if(W<=0&&W+x>0){if(this.activeSection===B)return;D.forEach(function(Oe){Oe.classList.remove(\"active\")});var J=N.querySelector('[href=\"#'.concat(B.getAttribute(\"id\"),'\"]'));if(J){J.classList.add(\"active\");var oe=J.closest(\"[data-hs-scrollspy-group]\");if(oe){var z=oe.querySelector(\"[href]\");z&&z.classList.add(\"active\")}}this.activeSection=B}}},{key:\"_scrollTo\",value:function(M){var O=M.$scrollspyEl,N=M.$scrollableEl,D=M.$link,B=document.querySelector(D.getAttribute(\"href\")),h=parseInt(this.getClassProperty(O,\"--scrollspy-offset\",\"0\")),G=this.getClassProperty(B,\"--scrollspy-offset\")||h,F=N===document?0:N.offsetTop,W=B.offsetTop-G-F,x=N===document?window:N;this._fireEvent(\"scroll\",O),this._dispatch(\"scroll.hs.scrollspy\",O,O),window.history.replaceState(null,null,D.getAttribute(\"href\")),x.scrollTo({top:W,left:0,behavior:\"smooth\"})}}])&&o(C.prototype,L),Object.defineProperty(C,\"prototype\",{writable:!1}),b}(S(765).Z);window.HSScrollspy=new P,document.addEventListener(\"load\",window.HSScrollspy.init())},51:(n,s,S)=>{function A(L){return A=typeof Symbol==\"function\"&&typeof Symbol.iterator==\"symbol\"?function(I){return typeof I}:function(I){return I&&typeof Symbol==\"function\"&&I.constructor===Symbol&&I!==Symbol.prototype?\"symbol\":typeof I},A(L)}function o(L){return function(I){if(Array.isArray(I))return i(I)}(L)||function(I){if(typeof Symbol<\"u\"&&I[Symbol.iterator]!=null||I[\"@@iterator\"]!=null)return Array.from(I)}(L)||function(I,u){if(I){if(typeof I==\"string\")return i(I,u);var H=Object.prototype.toString.call(I).slice(8,-1);return H===\"Object\"&&I.constructor&&(H=I.constructor.name),H===\"Map\"||H===\"Set\"?Array.from(I):H===\"Arguments\"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(H)?i(I,u):void 0}}(L)||function(){throw new TypeError(`Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}()}function i(L,I){(I==null||I>L.length)&&(I=L.length);for(var u=0,H=new Array(I);u<I;u++)H[u]=L[u];return H}function _(L,I){for(var u=0;u<I.length;u++){var H=I[u];H.enumerable=H.enumerable||!1,H.configurable=!0,\"value\"in H&&(H.writable=!0),Object.defineProperty(L,H.key,H)}}function c(L,I){return c=Object.setPrototypeOf||function(u,H){return u.__proto__=H,u},c(L,I)}function P(L,I){if(I&&(A(I)===\"object\"||typeof I==\"function\"))return I;if(I!==void 0)throw new TypeError(\"Derived constructors may only return object or undefined\");return function(u){if(u===void 0)throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");return u}(L)}function p(L){return p=Object.setPrototypeOf?Object.getPrototypeOf:function(I){return I.__proto__||Object.getPrototypeOf(I)},p(L)}var C=function(L){(function(N,D){if(typeof D!=\"function\"&&D!==null)throw new TypeError(\"Super expression must either be null or a function\");N.prototype=Object.create(D&&D.prototype,{constructor:{value:N,writable:!0,configurable:!0}}),Object.defineProperty(N,\"prototype\",{writable:!1}),D&&c(N,D)})(O,L);var I,u,H,b,M=(H=O,b=function(){if(typeof Reflect>\"u\"||!Reflect.construct||Reflect.construct.sham)return!1;if(typeof Proxy==\"function\")return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){})),!0}catch{return!1}}(),function(){var N,D=p(H);if(b){var B=p(this).constructor;N=Reflect.construct(D,arguments,B)}else N=D.apply(this,arguments);return P(this,N)});function O(){return function(N,D){if(!(N instanceof D))throw new TypeError(\"Cannot call a class as a function\")}(this,O),M.call(this,\"[data-hs-tab]\")}return I=O,(u=[{key:\"init\",value:function(){var N=this;document.addEventListener(\"keydown\",this._keyboardSupport.bind(this)),document.addEventListener(\"click\",function(D){var B=D.target.closest(N.selector);B&&N.open(B)}),document.querySelectorAll(\"[hs-data-tab-select]\").forEach(function(D){var B=document.querySelector(D.getAttribute(\"hs-data-tab-select\"));B&&B.addEventListener(\"change\",function(h){var G=document.querySelector('[data-hs-tab=\"'.concat(h.target.value,'\"]'));G&&N.open(G)})})}},{key:\"open\",value:function(N){var D=document.querySelector(N.getAttribute(\"data-hs-tab\")),B=o(N.parentElement.children),h=o(D.parentElement.children),G=N.closest(\"[hs-data-tab-select]\"),F=G?document.querySelector(G.getAttribute(\"data-hs-tab\")):null;B.forEach(function(W){return W.classList.remove(\"active\")}),h.forEach(function(W){return W.classList.add(\"hidden\")}),N.classList.add(\"active\"),D.classList.remove(\"hidden\"),this._fireEvent(\"change\",N),this._dispatch(\"change.hs.tab\",N,N),F&&(F.value=N.getAttribute(\"data-hs-tab\"))}},{key:\"_keyboardSupport\",value:function(N){var D=N.target.closest(this.selector);if(D){var B=D.closest('[role=\"tablist\"]').getAttribute(\"data-hs-tabs-vertical\")===\"true\";return(B?N.keyCode===38:N.keyCode===37)?(N.preventDefault(),this._left(D)):(B?N.keyCode===40:N.keyCode===39)?(N.preventDefault(),this._right(D)):N.keyCode===36?(N.preventDefault(),this._start(D)):N.keyCode===35?(N.preventDefault(),this._end(D)):void 0}}},{key:\"_right\",value:function(N){var D=N.closest('[role=\"tablist\"]');if(D){var B=o(D.querySelectorAll(this.selector)).filter(function(F){return!F.disabled}),h=D.querySelector(\"button:focus\"),G=B.findIndex(function(F){return F===h});G+1<B.length?G++:G=0,B[G].focus(),this.open(B[G])}}},{key:\"_left\",value:function(N){var D=N.closest('[role=\"tablist\"]');if(D){var B=o(D.querySelectorAll(this.selector)).filter(function(F){return!F.disabled}).reverse(),h=D.querySelector(\"button:focus\"),G=B.findIndex(function(F){return F===h});G+1<B.length?G++:G=0,B[G].focus(),this.open(B[G])}}},{key:\"_start\",value:function(N){var D=N.closest('[role=\"tablist\"]');if(D){var B=o(D.querySelectorAll(this.selector)).filter(function(h){return!h.disabled});B.length&&(B[0].focus(),this.open(B[0]))}}},{key:\"_end\",value:function(N){var D=N.closest('[role=\"tablist\"]');if(D){var B=o(D.querySelectorAll(this.selector)).reverse().filter(function(h){return!h.disabled});B.length&&(B[0].focus(),this.open(B[0]))}}}])&&_(I.prototype,u),Object.defineProperty(I,\"prototype\",{writable:!1}),O}(S(765).Z);window.HSTabs=new C,document.addEventListener(\"load\",window.HSTabs.init())},185:(n,s,S)=>{var A=S(765),o=S(714);function i(L){return i=typeof Symbol==\"function\"&&typeof Symbol.iterator==\"symbol\"?function(I){return typeof I}:function(I){return I&&typeof Symbol==\"function\"&&I.constructor===Symbol&&I!==Symbol.prototype?\"symbol\":typeof I},i(L)}function _(L,I){for(var u=0;u<I.length;u++){var H=I[u];H.enumerable=H.enumerable||!1,H.configurable=!0,\"value\"in H&&(H.writable=!0),Object.defineProperty(L,H.key,H)}}function c(L,I){return c=Object.setPrototypeOf||function(u,H){return u.__proto__=H,u},c(L,I)}function P(L,I){if(I&&(i(I)===\"object\"||typeof I==\"function\"))return I;if(I!==void 0)throw new TypeError(\"Derived constructors may only return object or undefined\");return function(u){if(u===void 0)throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");return u}(L)}function p(L){return p=Object.setPrototypeOf?Object.getPrototypeOf:function(I){return I.__proto__||Object.getPrototypeOf(I)},p(L)}var C=function(L){(function(N,D){if(typeof D!=\"function\"&&D!==null)throw new TypeError(\"Super expression must either be null or a function\");N.prototype=Object.create(D&&D.prototype,{constructor:{value:N,writable:!0,configurable:!0}}),Object.defineProperty(N,\"prototype\",{writable:!1}),D&&c(N,D)})(O,L);var I,u,H,b,M=(H=O,b=function(){if(typeof Reflect>\"u\"||!Reflect.construct||Reflect.construct.sham)return!1;if(typeof Proxy==\"function\")return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){})),!0}catch{return!1}}(),function(){var N,D=p(H);if(b){var B=p(this).constructor;N=Reflect.construct(D,arguments,B)}else N=D.apply(this,arguments);return P(this,N)});function O(){return function(N,D){if(!(N instanceof D))throw new TypeError(\"Cannot call a class as a function\")}(this,O),M.call(this,\".hs-tooltip\")}return I=O,(u=[{key:\"init\",value:function(){var N=this;document.addEventListener(\"click\",function(D){var B=D.target.closest(N.selector);B&&N.getClassProperty(B,\"--trigger\")===\"focus\"&&N._focus(B),B&&N.getClassProperty(B,\"--trigger\")===\"click\"&&N._click(B)}),document.addEventListener(\"mousemove\",function(D){var B=D.target.closest(N.selector);B&&N.getClassProperty(B,\"--trigger\")!==\"focus\"&&N.getClassProperty(B,\"--trigger\")!==\"click\"&&N._hover(B)})}},{key:\"_hover\",value:function(N){var D=this;if(!N.classList.contains(\"show\")){var B=N.querySelector(\".hs-tooltip-toggle\"),h=N.querySelector(\".hs-tooltip-content\"),G=this.getClassProperty(N,\"--placement\");(0,o.fi)(B,h,{placement:G||\"top\",strategy:\"fixed\",modifiers:[{name:\"offset\",options:{offset:[0,5]}}]}),this.show(N),N.addEventListener(\"mouseleave\",function F(W){W.relatedTarget.closest(D.selector)&&W.relatedTarget.closest(D.selector)==N||(D.hide(N),N.removeEventListener(\"mouseleave\",F,!0))},!0)}}},{key:\"_focus\",value:function(N){var D=this,B=N.querySelector(\".hs-tooltip-toggle\"),h=N.querySelector(\".hs-tooltip-content\"),G=this.getClassProperty(N,\"--placement\"),F=this.getClassProperty(N,\"--strategy\");(0,o.fi)(B,h,{placement:G||\"top\",strategy:F||\"fixed\",modifiers:[{name:\"offset\",options:{offset:[0,5]}}]}),this.show(N),N.addEventListener(\"blur\",function W(){D.hide(N),N.removeEventListener(\"blur\",W,!0)},!0)}},{key:\"_click\",value:function(N){var D=this;if(!N.classList.contains(\"show\")){var B=N.querySelector(\".hs-tooltip-toggle\"),h=N.querySelector(\".hs-tooltip-content\"),G=this.getClassProperty(N,\"--placement\"),F=this.getClassProperty(N,\"--strategy\");(0,o.fi)(B,h,{placement:G||\"top\",strategy:F||\"fixed\",modifiers:[{name:\"offset\",options:{offset:[0,5]}}]}),this.show(N);var W=function x(J){setTimeout(function(){D.hide(N),N.removeEventListener(\"click\",x,!0),N.removeEventListener(\"blur\",x,!0)})};N.addEventListener(\"blur\",W,!0),N.addEventListener(\"click\",W,!0)}}},{key:\"show\",value:function(N){var D=this;N.querySelector(\".hs-tooltip-content\").classList.remove(\"hidden\"),setTimeout(function(){N.classList.add(\"show\"),D._fireEvent(\"show\",N),D._dispatch(\"show.hs.tooltip\",N,N)})}},{key:\"hide\",value:function(N){var D=N.querySelector(\".hs-tooltip-content\");N.classList.remove(\"show\"),this._fireEvent(\"hide\",N),this._dispatch(\"hide.hs.tooltip\",N,N),this.afterTransition(D,function(){N.classList.contains(\"show\")||D.classList.add(\"hidden\")})}}])&&_(I.prototype,u),Object.defineProperty(I,\"prototype\",{writable:!1}),O}(A.Z);window.HSTooltip=new C,document.addEventListener(\"load\",window.HSTooltip.init())},765:(n,s,S)=>{function A(i,_){for(var c=0;c<_.length;c++){var P=_[c];P.enumerable=P.enumerable||!1,P.configurable=!0,\"value\"in P&&(P.writable=!0),Object.defineProperty(i,P.key,P)}}S.d(s,{Z:()=>o});var o=function(){function i(P,p){(function(C,L){if(!(C instanceof L))throw new TypeError(\"Cannot call a class as a function\")})(this,i),this.$collection=[],this.selector=P,this.config=p,this.events={}}var _,c;return _=i,c=[{key:\"_fireEvent\",value:function(P){var p=arguments.length>1&&arguments[1]!==void 0?arguments[1]:null;this.events.hasOwnProperty(P)&&this.events[P](p)}},{key:\"_dispatch\",value:function(P,p){var C=arguments.length>2&&arguments[2]!==void 0?arguments[2]:null,L=new CustomEvent(P,{detail:{payload:C},bubbles:!0,cancelable:!0,composed:!1});p.dispatchEvent(L)}},{key:\"on\",value:function(P,p){this.events[P]=p}},{key:\"afterTransition\",value:function(P,p){window.getComputedStyle(P,null).getPropertyValue(\"transition\")!==\"all 0s ease 0s\"?P.addEventListener(\"transitionend\",function C(){p(),P.removeEventListener(\"transitionend\",C,!0)},!0):p()}},{key:\"getClassProperty\",value:function(P,p){var C=arguments.length>2&&arguments[2]!==void 0?arguments[2]:\"\",L=(window.getComputedStyle(P).getPropertyValue(p)||C).replace(\" \",\"\");return L}}],c&&A(_.prototype,c),Object.defineProperty(_,\"prototype\",{writable:!1}),i}()},714:(n,s,S)=>{function A(U){if(U==null)return window;if(U.toString()!==\"[object Window]\"){var d=U.ownerDocument;return d&&d.defaultView||window}return U}function o(U){return U instanceof A(U).Element||U instanceof Element}function i(U){return U instanceof A(U).HTMLElement||U instanceof HTMLElement}function _(U){return typeof ShadowRoot<\"u\"&&(U instanceof A(U).ShadowRoot||U instanceof ShadowRoot)}S.d(s,{fi:()=>tn});var c=Math.max,P=Math.min,p=Math.round;function C(U,d){d===void 0&&(d=!1);var g=U.getBoundingClientRect(),w=1,Z=1;if(i(U)&&d){var q=U.offsetHeight,Q=U.offsetWidth;Q>0&&(w=p(g.width)/Q||1),q>0&&(Z=p(g.height)/q||1)}return{width:g.width/w,height:g.height/Z,top:g.top/Z,right:g.right/w,bottom:g.bottom/Z,left:g.left/w,x:g.left/w,y:g.top/Z}}function L(U){var d=A(U);return{scrollLeft:d.pageXOffset,scrollTop:d.pageYOffset}}function I(U){return U?(U.nodeName||\"\").toLowerCase():null}function u(U){return((o(U)?U.ownerDocument:U.document)||window.document).documentElement}function H(U){return C(u(U)).left+L(U).scrollLeft}function b(U){return A(U).getComputedStyle(U)}function M(U){var d=b(U),g=d.overflow,w=d.overflowX,Z=d.overflowY;return/auto|scroll|overlay|hidden/.test(g+Z+w)}function O(U,d,g){g===void 0&&(g=!1);var w,Z,q=i(d),Q=i(d)&&function(re){var We=re.getBoundingClientRect(),ae=p(We.width)/re.offsetWidth||1,Me=p(We.height)/re.offsetHeight||1;return ae!==1||Me!==1}(d),ee=u(d),Ee=C(U,Q),ne={scrollLeft:0,scrollTop:0},Ae={x:0,y:0};return(q||!q&&!g)&&((I(d)!==\"body\"||M(ee))&&(ne=(w=d)!==A(w)&&i(w)?{scrollLeft:(Z=w).scrollLeft,scrollTop:Z.scrollTop}:L(w)),i(d)?((Ae=C(d,!0)).x+=d.clientLeft,Ae.y+=d.clientTop):ee&&(Ae.x=H(ee))),{x:Ee.left+ne.scrollLeft-Ae.x,y:Ee.top+ne.scrollTop-Ae.y,width:Ee.width,height:Ee.height}}function N(U){var d=C(U),g=U.offsetWidth,w=U.offsetHeight;return Math.abs(d.width-g)<=1&&(g=d.width),Math.abs(d.height-w)<=1&&(w=d.height),{x:U.offsetLeft,y:U.offsetTop,width:g,height:w}}function D(U){return I(U)===\"html\"?U:U.assignedSlot||U.parentNode||(_(U)?U.host:null)||u(U)}function B(U){return[\"html\",\"body\",\"#document\"].indexOf(I(U))>=0?U.ownerDocument.body:i(U)&&M(U)?U:B(D(U))}function h(U,d){var g;d===void 0&&(d=[]);var w=B(U),Z=w===((g=U.ownerDocument)==null?void 0:g.body),q=A(w),Q=Z?[q].concat(q.visualViewport||[],M(w)?w:[]):w,ee=d.concat(Q);return Z?ee:ee.concat(h(D(Q)))}function G(U){return[\"table\",\"td\",\"th\"].indexOf(I(U))>=0}function F(U){return i(U)&&b(U).position!==\"fixed\"?U.offsetParent:null}function W(U){for(var d=A(U),g=F(U);g&&G(g)&&b(g).position===\"static\";)g=F(g);return g&&(I(g)===\"html\"||I(g)===\"body\"&&b(g).position===\"static\")?d:g||function(w){var Z=navigator.userAgent.toLowerCase().indexOf(\"firefox\")!==-1;if(navigator.userAgent.indexOf(\"Trident\")!==-1&&i(w)&&b(w).position===\"fixed\")return null;for(var q=D(w);i(q)&&[\"html\",\"body\"].indexOf(I(q))<0;){var Q=b(q);if(Q.transform!==\"none\"||Q.perspective!==\"none\"||Q.contain===\"paint\"||[\"transform\",\"perspective\"].indexOf(Q.willChange)!==-1||Z&&Q.willChange===\"filter\"||Z&&Q.filter&&Q.filter!==\"none\")return q;q=q.parentNode}return null}(U)||d}var x=\"top\",J=\"bottom\",oe=\"right\",z=\"left\",Oe=\"auto\",Ue=[x,J,oe,z],ye=\"start\",TE=\"end\",dE=\"viewport\",me=\"popper\",fE=Ue.reduce(function(U,d){return U.concat([d+\"-\"+ye,d+\"-\"+TE])},[]),rE=[].concat(Ue,[Oe]).reduce(function(U,d){return U.concat([d,d+\"-\"+ye,d+\"-\"+TE])},[]),pE=[\"beforeRead\",\"read\",\"afterRead\",\"beforeMain\",\"main\",\"afterMain\",\"beforeWrite\",\"write\",\"afterWrite\"];function Tt(U){var d=new Map,g=new Set,w=[];function Z(q){g.add(q.name),[].concat(q.requires||[],q.requiresIfExists||[]).forEach(function(Q){if(!g.has(Q)){var ee=d.get(Q);ee&&Z(ee)}}),w.push(q)}return U.forEach(function(q){d.set(q.name,q)}),U.forEach(function(q){g.has(q.name)||Z(q)}),w}var rt={placement:\"bottom\",modifiers:[],strategy:\"absolute\"};function Ve(){for(var U=arguments.length,d=new Array(U),g=0;g<U;g++)d[g]=arguments[g];return!d.some(function(w){return!(w&&typeof w.getBoundingClientRect==\"function\")})}function ME(U){U===void 0&&(U={});var d=U,g=d.defaultModifiers,w=g===void 0?[]:g,Z=d.defaultOptions,q=Z===void 0?rt:Z;return function(Q,ee,Ee){Ee===void 0&&(Ee=q);var ne,Ae,re={placement:\"bottom\",orderedModifiers:[],options:Object.assign({},rt,q),modifiersData:{},elements:{reference:Q,popper:ee},attributes:{},styles:{}},We=[],ae=!1,Me={state:re,setOptions:function(ie){var we=typeof ie==\"function\"?ie(re.options):ie;de(),re.options=Object.assign({},q,re.options,we),re.scrollParents={reference:o(Q)?h(Q):Q.contextElement?h(Q.contextElement):[],popper:h(ee)};var be,ce,he=function(Ie){var le=Tt(Ie);return pE.reduce(function(fe,pe){return fe.concat(le.filter(function(He){return He.phase===pe}))},[])}((be=[].concat(w,re.options.modifiers),ce=be.reduce(function(Ie,le){var fe=Ie[le.name];return Ie[le.name]=fe?Object.assign({},fe,le,{options:Object.assign({},fe.options,le.options),data:Object.assign({},fe.data,le.data)}):le,Ie},{}),Object.keys(ce).map(function(Ie){return ce[Ie]})));return re.orderedModifiers=he.filter(function(Ie){return Ie.enabled}),re.orderedModifiers.forEach(function(Ie){var le=Ie.name,fe=Ie.options,pe=fe===void 0?{}:fe,He=Ie.effect;if(typeof He==\"function\"){var ke=He({state:re,name:le,instance:Me,options:pe});We.push(ke||function(){})}}),Me.update()},forceUpdate:function(){if(!ae){var ie=re.elements,we=ie.reference,be=ie.popper;if(Ve(we,be)){re.rects={reference:O(we,W(be),re.options.strategy===\"fixed\"),popper:N(be)},re.reset=!1,re.placement=re.options.placement,re.orderedModifiers.forEach(function(He){return re.modifiersData[He.name]=Object.assign({},He.data)});for(var ce=0;ce<re.orderedModifiers.length;ce++)if(re.reset!==!0){var he=re.orderedModifiers[ce],Ie=he.fn,le=he.options,fe=le===void 0?{}:le,pe=he.name;typeof Ie==\"function\"&&(re=Ie({state:re,options:fe,name:pe,instance:Me})||re)}else re.reset=!1,ce=-1}}},update:(ne=function(){return new Promise(function(ie){Me.forceUpdate(),ie(re)})},function(){return Ae||(Ae=new Promise(function(ie){Promise.resolve().then(function(){Ae=void 0,ie(ne())})})),Ae}),destroy:function(){de(),ae=!0}};if(!Ve(Q,ee))return Me;function de(){We.forEach(function(ie){return ie()}),We=[]}return Me.setOptions(Ee).then(function(ie){!ae&&Ee.onFirstUpdate&&Ee.onFirstUpdate(ie)}),Me}}var $e={passive:!0};function ze(U){return U.split(\"-\")[0]}function ve(U){return U.split(\"-\")[1]}function Fe(U){return[\"top\",\"bottom\"].indexOf(U)>=0?\"x\":\"y\"}function Te(U){var d,g=U.reference,w=U.element,Z=U.placement,q=Z?ze(Z):null,Q=Z?ve(Z):null,ee=g.x+g.width/2-w.width/2,Ee=g.y+g.height/2-w.height/2;switch(q){case x:d={x:ee,y:g.y-w.height};break;case J:d={x:ee,y:g.y+g.height};break;case oe:d={x:g.x+g.width,y:Ee};break;case z:d={x:g.x-w.width,y:Ee};break;default:d={x:g.x,y:g.y}}var ne=q?Fe(q):null;if(ne!=null){var Ae=ne===\"y\"?\"height\":\"width\";switch(Q){case ye:d[ne]=d[ne]-(g[Ae]/2-w[Ae]/2);break;case TE:d[ne]=d[ne]+(g[Ae]/2-w[Ae]/2)}}return d}var Xe={top:\"auto\",right:\"auto\",bottom:\"auto\",left:\"auto\"};function se(U){var d,g=U.popper,w=U.popperRect,Z=U.placement,q=U.variation,Q=U.offsets,ee=U.position,Ee=U.gpuAcceleration,ne=U.adaptive,Ae=U.roundOffsets,re=U.isFixed,We=Q.x,ae=We===void 0?0:We,Me=Q.y,de=Me===void 0?0:Me,ie=typeof Ae==\"function\"?Ae({x:ae,y:de}):{x:ae,y:de};ae=ie.x,de=ie.y;var we=Q.hasOwnProperty(\"x\"),be=Q.hasOwnProperty(\"y\"),ce=z,he=x,Ie=window;if(ne){var le=W(g),fe=\"clientHeight\",pe=\"clientWidth\";le===A(g)&&b(le=u(g)).position!==\"static\"&&ee===\"absolute\"&&(fe=\"scrollHeight\",pe=\"scrollWidth\"),le=le,(Z===x||(Z===z||Z===oe)&&q===TE)&&(he=J,de-=(re&&Ie.visualViewport?Ie.visualViewport.height:le[fe])-w.height,de*=Ee?1:-1),Z!==z&&(Z!==x&&Z!==J||q!==TE)||(ce=oe,ae-=(re&&Ie.visualViewport?Ie.visualViewport.width:le[pe])-w.width,ae*=Ee?1:-1)}var He,ke=Object.assign({position:ee},ne&&Xe),Ke=Ae===!0?function(sE){var lE=sE.x,PE=sE.y,SE=window.devicePixelRatio||1;return{x:p(lE*SE)/SE||0,y:p(PE*SE)/SE||0}}({x:ae,y:de}):{x:ae,y:de};return ae=Ke.x,de=Ke.y,Ee?Object.assign({},ke,((He={})[he]=be?\"0\":\"\",He[ce]=we?\"0\":\"\",He.transform=(Ie.devicePixelRatio||1)<=1?\"translate(\"+ae+\"px, \"+de+\"px)\":\"translate3d(\"+ae+\"px, \"+de+\"px, 0)\",He)):Object.assign({},ke,((d={})[he]=be?de+\"px\":\"\",d[ce]=we?ae+\"px\":\"\",d.transform=\"\",d))}var gE={left:\"right\",right:\"left\",bottom:\"top\",top:\"bottom\"};function Nt(U){return U.replace(/left|right|bottom|top/g,function(d){return gE[d]})}var En={start:\"end\",end:\"start\"};function dT(U){return U.replace(/start|end/g,function(d){return En[d]})}function pT(U,d){var g=d.getRootNode&&d.getRootNode();if(U.contains(d))return!0;if(g&&_(g)){var w=d;do{if(w&&U.isSameNode(w))return!0;w=w.parentNode||w.host}while(w)}return!1}function Bt(U){return Object.assign({},U,{left:U.x,top:U.y,right:U.x+U.width,bottom:U.y+U.height})}function MT(U,d){return d===dE?Bt(function(g){var w=A(g),Z=u(g),q=w.visualViewport,Q=Z.clientWidth,ee=Z.clientHeight,Ee=0,ne=0;return q&&(Q=q.width,ee=q.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(Ee=q.offsetLeft,ne=q.offsetTop)),{width:Q,height:ee,x:Ee+H(g),y:ne}}(U)):o(d)?function(g){var w=C(g);return w.top=w.top+g.clientTop,w.left=w.left+g.clientLeft,w.bottom=w.top+g.clientHeight,w.right=w.left+g.clientWidth,w.width=g.clientWidth,w.height=g.clientHeight,w.x=w.left,w.y=w.top,w}(d):Bt(function(g){var w,Z=u(g),q=L(g),Q=(w=g.ownerDocument)==null?void 0:w.body,ee=c(Z.scrollWidth,Z.clientWidth,Q?Q.scrollWidth:0,Q?Q.clientWidth:0),Ee=c(Z.scrollHeight,Z.clientHeight,Q?Q.scrollHeight:0,Q?Q.clientHeight:0),ne=-q.scrollLeft+H(g),Ae=-q.scrollTop;return b(Q||Z).direction===\"rtl\"&&(ne+=c(Z.clientWidth,Q?Q.clientWidth:0)-ee),{width:ee,height:Ee,x:ne,y:Ae}}(u(U)))}function UT(U){return Object.assign({},{top:0,right:0,bottom:0,left:0},U)}function mT(U,d){return d.reduce(function(g,w){return g[w]=U,g},{})}function Rt(U,d){d===void 0&&(d={});var g=d,w=g.placement,Z=w===void 0?U.placement:w,q=g.boundary,Q=q===void 0?\"clippingParents\":q,ee=g.rootBoundary,Ee=ee===void 0?dE:ee,ne=g.elementContext,Ae=ne===void 0?me:ne,re=g.altBoundary,We=re!==void 0&&re,ae=g.padding,Me=ae===void 0?0:ae,de=UT(typeof Me!=\"number\"?Me:mT(Me,Ue)),ie=Ae===me?\"reference\":me,we=U.rects.popper,be=U.elements[We?ie:Ae],ce=function(Ke,sE,lE){var PE=sE===\"clippingParents\"?function(Be){var UE=h(D(Be)),oE=[\"absolute\",\"fixed\"].indexOf(b(Be).position)>=0&&i(Be)?W(Be):Be;return o(oE)?UE.filter(function(RE){return o(RE)&&pT(RE,oE)&&I(RE)!==\"body\"}):[]}(Ke):[].concat(sE),SE=[].concat(PE,[lE]),tE=SE[0],xe=SE.reduce(function(Be,UE){var oE=MT(Ke,UE);return Be.top=c(oE.top,Be.top),Be.right=P(oE.right,Be.right),Be.bottom=P(oE.bottom,Be.bottom),Be.left=c(oE.left,Be.left),Be},MT(Ke,tE));return xe.width=xe.right-xe.left,xe.height=xe.bottom-xe.top,xe.x=xe.left,xe.y=xe.top,xe}(o(be)?be:be.contextElement||u(U.elements.popper),Q,Ee),he=C(U.elements.reference),Ie=Te({reference:he,element:we,strategy:\"absolute\",placement:Z}),le=Bt(Object.assign({},we,Ie)),fe=Ae===me?le:he,pe={top:ce.top-fe.top+de.top,bottom:fe.bottom-ce.bottom+de.bottom,left:ce.left-fe.left+de.left,right:fe.right-ce.right+de.right},He=U.modifiersData.offset;if(Ae===me&&He){var ke=He[Z];Object.keys(pe).forEach(function(Ke){var sE=[oe,J].indexOf(Ke)>=0?1:-1,lE=[x,J].indexOf(Ke)>=0?\"y\":\"x\";pe[Ke]+=ke[lE]*sE})}return pe}function nt(U,d,g){return c(U,P(d,g))}function hT(U,d,g){return g===void 0&&(g={x:0,y:0}),{top:U.top-d.height-g.y,right:U.right-d.width+g.x,bottom:U.bottom-d.height+g.y,left:U.left-d.width-g.x}}function GT(U){return[x,oe,J,z].some(function(d){return U[d]>=0})}var tn=ME({defaultModifiers:[{name:\"eventListeners\",enabled:!0,phase:\"write\",fn:function(){},effect:function(U){var d=U.state,g=U.instance,w=U.options,Z=w.scroll,q=Z===void 0||Z,Q=w.resize,ee=Q===void 0||Q,Ee=A(d.elements.popper),ne=[].concat(d.scrollParents.reference,d.scrollParents.popper);return q&&ne.forEach(function(Ae){Ae.addEventListener(\"scroll\",g.update,$e)}),ee&&Ee.addEventListener(\"resize\",g.update,$e),function(){q&&ne.forEach(function(Ae){Ae.removeEventListener(\"scroll\",g.update,$e)}),ee&&Ee.removeEventListener(\"resize\",g.update,$e)}},data:{}},{name:\"popperOffsets\",enabled:!0,phase:\"read\",fn:function(U){var d=U.state,g=U.name;d.modifiersData[g]=Te({reference:d.rects.reference,element:d.rects.popper,strategy:\"absolute\",placement:d.placement})},data:{}},{name:\"computeStyles\",enabled:!0,phase:\"beforeWrite\",fn:function(U){var d=U.state,g=U.options,w=g.gpuAcceleration,Z=w===void 0||w,q=g.adaptive,Q=q===void 0||q,ee=g.roundOffsets,Ee=ee===void 0||ee,ne={placement:ze(d.placement),variation:ve(d.placement),popper:d.elements.popper,popperRect:d.rects.popper,gpuAcceleration:Z,isFixed:d.options.strategy===\"fixed\"};d.modifiersData.popperOffsets!=null&&(d.styles.popper=Object.assign({},d.styles.popper,se(Object.assign({},ne,{offsets:d.modifiersData.popperOffsets,position:d.options.strategy,adaptive:Q,roundOffsets:Ee})))),d.modifiersData.arrow!=null&&(d.styles.arrow=Object.assign({},d.styles.arrow,se(Object.assign({},ne,{offsets:d.modifiersData.arrow,position:\"absolute\",adaptive:!1,roundOffsets:Ee})))),d.attributes.popper=Object.assign({},d.attributes.popper,{\"data-popper-placement\":d.placement})},data:{}},{name:\"applyStyles\",enabled:!0,phase:\"write\",fn:function(U){var d=U.state;Object.keys(d.elements).forEach(function(g){var w=d.styles[g]||{},Z=d.attributes[g]||{},q=d.elements[g];i(q)&&I(q)&&(Object.assign(q.style,w),Object.keys(Z).forEach(function(Q){var ee=Z[Q];ee===!1?q.removeAttribute(Q):q.setAttribute(Q,ee===!0?\"\":ee)}))})},effect:function(U){var d=U.state,g={popper:{position:d.options.strategy,left:\"0\",top:\"0\",margin:\"0\"},arrow:{position:\"absolute\"},reference:{}};return Object.assign(d.elements.popper.style,g.popper),d.styles=g,d.elements.arrow&&Object.assign(d.elements.arrow.style,g.arrow),function(){Object.keys(d.elements).forEach(function(w){var Z=d.elements[w],q=d.attributes[w]||{},Q=Object.keys(d.styles.hasOwnProperty(w)?d.styles[w]:g[w]).reduce(function(ee,Ee){return ee[Ee]=\"\",ee},{});i(Z)&&I(Z)&&(Object.assign(Z.style,Q),Object.keys(q).forEach(function(ee){Z.removeAttribute(ee)}))})}},requires:[\"computeStyles\"]},{name:\"offset\",enabled:!0,phase:\"main\",requires:[\"popperOffsets\"],fn:function(U){var d=U.state,g=U.options,w=U.name,Z=g.offset,q=Z===void 0?[0,0]:Z,Q=rE.reduce(function(Ae,re){return Ae[re]=function(We,ae,Me){var de=ze(We),ie=[z,x].indexOf(de)>=0?-1:1,we=typeof Me==\"function\"?Me(Object.assign({},ae,{placement:We})):Me,be=we[0],ce=we[1];return be=be||0,ce=(ce||0)*ie,[z,oe].indexOf(de)>=0?{x:ce,y:be}:{x:be,y:ce}}(re,d.rects,q),Ae},{}),ee=Q[d.placement],Ee=ee.x,ne=ee.y;d.modifiersData.popperOffsets!=null&&(d.modifiersData.popperOffsets.x+=Ee,d.modifiersData.popperOffsets.y+=ne),d.modifiersData[w]=Q}},{name:\"flip\",enabled:!0,phase:\"main\",fn:function(U){var d=U.state,g=U.options,w=U.name;if(!d.modifiersData[w]._skip){for(var Z=g.mainAxis,q=Z===void 0||Z,Q=g.altAxis,ee=Q===void 0||Q,Ee=g.fallbackPlacements,ne=g.padding,Ae=g.boundary,re=g.rootBoundary,We=g.altBoundary,ae=g.flipVariations,Me=ae===void 0||ae,de=g.allowedAutoPlacements,ie=d.options.placement,we=ze(ie),be=Ee||(we!==ie&&Me?function(RE){if(ze(RE)===Oe)return[];var _E=Nt(RE);return[dT(RE),_E,dT(_E)]}(ie):[Nt(ie)]),ce=[ie].concat(be).reduce(function(RE,_E){return RE.concat(ze(_E)===Oe?function($E,mE){mE===void 0&&(mE={});var LE=mE,lt=LE.placement,_t=LE.boundary,xE=LE.rootBoundary,vt=LE.padding,Ft=LE.flipVariations,XE=LE.allowedAutoPlacements,Yt=XE===void 0?rE:XE,At=ve(lt),Lt=At?Ft?fE:fE.filter(function(cE){return ve(cE)===At}):Ue,kE=Lt.filter(function(cE){return Yt.indexOf(cE)>=0});kE.length===0&&(kE=Lt);var KE=kE.reduce(function(cE,HE){return cE[HE]=Rt($E,{placement:HE,boundary:_t,rootBoundary:xE,padding:vt})[ze(HE)],cE},{});return Object.keys(KE).sort(function(cE,HE){return KE[cE]-KE[HE]})}(d,{placement:_E,boundary:Ae,rootBoundary:re,padding:ne,flipVariations:Me,allowedAutoPlacements:de}):_E)},[]),he=d.rects.reference,Ie=d.rects.popper,le=new Map,fe=!0,pe=ce[0],He=0;He<ce.length;He++){var ke=ce[He],Ke=ze(ke),sE=ve(ke)===ye,lE=[x,J].indexOf(Ke)>=0,PE=lE?\"width\":\"height\",SE=Rt(d,{placement:ke,boundary:Ae,rootBoundary:re,altBoundary:We,padding:ne}),tE=lE?sE?oe:z:sE?J:x;he[PE]>Ie[PE]&&(tE=Nt(tE));var xe=Nt(tE),Be=[];if(q&&Be.push(SE[Ke]<=0),ee&&Be.push(SE[tE]<=0,SE[xe]<=0),Be.every(function(RE){return RE})){pe=ke,fe=!1;break}le.set(ke,Be)}if(fe)for(var UE=function(RE){var _E=ce.find(function($E){var mE=le.get($E);if(mE)return mE.slice(0,RE).every(function(LE){return LE})});if(_E)return pe=_E,\"break\"},oE=Me?3:1;oE>0&&UE(oE)!==\"break\";oE--);d.placement!==pe&&(d.modifiersData[w]._skip=!0,d.placement=pe,d.reset=!0)}},requiresIfExists:[\"offset\"],data:{_skip:!1}},{name:\"preventOverflow\",enabled:!0,phase:\"main\",fn:function(U){var d=U.state,g=U.options,w=U.name,Z=g.mainAxis,q=Z===void 0||Z,Q=g.altAxis,ee=Q!==void 0&&Q,Ee=g.boundary,ne=g.rootBoundary,Ae=g.altBoundary,re=g.padding,We=g.tether,ae=We===void 0||We,Me=g.tetherOffset,de=Me===void 0?0:Me,ie=Rt(d,{boundary:Ee,rootBoundary:ne,padding:re,altBoundary:Ae}),we=ze(d.placement),be=ve(d.placement),ce=!be,he=Fe(we),Ie=he===\"x\"?\"y\":\"x\",le=d.modifiersData.popperOffsets,fe=d.rects.reference,pe=d.rects.popper,He=typeof de==\"function\"?de(Object.assign({},d.rects,{placement:d.placement})):de,ke=typeof He==\"number\"?{mainAxis:He,altAxis:He}:Object.assign({mainAxis:0,altAxis:0},He),Ke=d.modifiersData.offset?d.modifiersData.offset[d.placement]:null,sE={x:0,y:0};if(le){if(q){var lE,PE=he===\"y\"?x:z,SE=he===\"y\"?J:oe,tE=he===\"y\"?\"height\":\"width\",xe=le[he],Be=xe+ie[PE],UE=xe-ie[SE],oE=ae?-pe[tE]/2:0,RE=be===ye?fe[tE]:pe[tE],_E=be===ye?-pe[tE]:-fe[tE],$E=d.elements.arrow,mE=ae&&$E?N($E):{width:0,height:0},LE=d.modifiersData[\"arrow#persistent\"]?d.modifiersData[\"arrow#persistent\"].padding:{top:0,right:0,bottom:0,left:0},lt=LE[PE],_t=LE[SE],xE=nt(0,fe[tE],mE[tE]),vt=ce?fe[tE]/2-oE-xE-lt-ke.mainAxis:RE-xE-lt-ke.mainAxis,Ft=ce?-fe[tE]/2+oE+xE+_t+ke.mainAxis:_E+xE+_t+ke.mainAxis,XE=d.elements.arrow&&W(d.elements.arrow),Yt=XE?he===\"y\"?XE.clientTop||0:XE.clientLeft||0:0,At=(lE=Ke==null?void 0:Ke[he])!=null?lE:0,Lt=xe+Ft-At,kE=nt(ae?P(Be,xe+vt-At-Yt):Be,xe,ae?c(UE,Lt):UE);le[he]=kE,sE[he]=kE-xe}if(ee){var KE,cE=he===\"x\"?x:z,HE=he===\"x\"?J:oe,bE=le[Ie],Ct=Ie===\"y\"?\"height\":\"width\",gT=bE+ie[cE],HT=bE-ie[HE],Vt=[x,z].indexOf(we)!==-1,bT=(KE=Ke==null?void 0:Ke[Ie])!=null?KE:0,yT=Vt?gT:bE-fe[Ct]-pe[Ct]-bT+ke.altAxis,BT=Vt?bE+fe[Ct]+pe[Ct]-bT-ke.altAxis:HT,vT=ae&&Vt?function(Tn,rn,Wt){var FT=nt(Tn,rn,Wt);return FT>Wt?Wt:FT}(yT,bE,BT):nt(ae?yT:gT,bE,ae?BT:HT);le[Ie]=vT,sE[Ie]=vT-bE}d.modifiersData[w]=sE}},requiresIfExists:[\"offset\"]},{name:\"arrow\",enabled:!0,phase:\"main\",fn:function(U){var d,g=U.state,w=U.name,Z=U.options,q=g.elements.arrow,Q=g.modifiersData.popperOffsets,ee=ze(g.placement),Ee=Fe(ee),ne=[z,oe].indexOf(ee)>=0?\"height\":\"width\";if(q&&Q){var Ae=function(pe,He){return UT(typeof(pe=typeof pe==\"function\"?pe(Object.assign({},He.rects,{placement:He.placement})):pe)!=\"number\"?pe:mT(pe,Ue))}(Z.padding,g),re=N(q),We=Ee===\"y\"?x:z,ae=Ee===\"y\"?J:oe,Me=g.rects.reference[ne]+g.rects.reference[Ee]-Q[Ee]-g.rects.popper[ne],de=Q[Ee]-g.rects.reference[Ee],ie=W(q),we=ie?Ee===\"y\"?ie.clientHeight||0:ie.clientWidth||0:0,be=Me/2-de/2,ce=Ae[We],he=we-re[ne]-Ae[ae],Ie=we/2-re[ne]/2+be,le=nt(ce,Ie,he),fe=Ee;g.modifiersData[w]=((d={})[fe]=le,d.centerOffset=le-Ie,d)}},effect:function(U){var d=U.state,g=U.options.element,w=g===void 0?\"[data-popper-arrow]\":g;w!=null&&(typeof w!=\"string\"||(w=d.elements.popper.querySelector(w)))&&pT(d.elements.popper,w)&&(d.elements.arrow=w)},requires:[\"popperOffsets\"],requiresIfExists:[\"preventOverflow\"]},{name:\"hide\",enabled:!0,phase:\"main\",requiresIfExists:[\"preventOverflow\"],fn:function(U){var d=U.state,g=U.name,w=d.rects.reference,Z=d.rects.popper,q=d.modifiersData.preventOverflow,Q=Rt(d,{elementContext:\"reference\"}),ee=Rt(d,{altBoundary:!0}),Ee=hT(Q,w),ne=hT(ee,Z,q),Ae=GT(Ee),re=GT(ne);d.modifiersData[g]={referenceClippingOffsets:Ee,popperEscapeOffsets:ne,isReferenceHidden:Ae,hasPopperEscaped:re},d.attributes.popper=Object.assign({},d.attributes.popper,{\"data-popper-reference-hidden\":Ae,\"data-popper-escaped\":re})}}]})}},t={};function r(n){var s=t[n];if(s!==void 0)return s.exports;var S=t[n]={exports:{}};return T[n](S,S.exports,r),S.exports}r.d=(n,s)=>{for(var S in s)r.o(s,S)&&!r.o(n,S)&&Object.defineProperty(n,S,{enumerable:!0,get:s[S]})},r.o=(n,s)=>Object.prototype.hasOwnProperty.call(n,s),r.r=n=>{typeof Symbol<\"u\"&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(n,\"__esModule\",{value:!0})};var R={};return r.r(R),r(661),r(795),r(682),r(284),r(181),r(778),r(51),r(185),R})()})})(Qn);function Zn(E){let e=E[0].title+\"\",T;return{c(){T=te(e)},m(t,r){V(t,T,r)},p(t,r){r&1&&e!==(e=t[0].title+\"\")&&Le(T,e)},d(t){t&&Y(T)}}}function jn(E){let e,T;return{c(){e=te(\"Welcome to \"),T=f(\"span\"),T.textContent=\"Vanna.AI\",a(T,\"class\",\"nav-title\")},m(t,r){V(t,e,r),V(t,T,r)},p:j,d(t){t&&(Y(e),Y(T))}}}function zn(E){let e,T,t,r,R=E[0].subtitle+\"\",n;function s(o,i){return o[0].title==\"Welcome to Vanna.AI\"?jn:Zn}let S=s(E),A=S(E);return{c(){e=f(\"div\"),T=f(\"h1\"),A.c(),t=$(),r=f(\"p\"),n=te(R),a(T,\"class\",\"text-3xl font-bold text-gray-800 sm:text-4xl dark:text-white\"),a(r,\"class\",\"mt-3 text-gray-600 dark:text-gray-400\"),a(e,\"class\",\"max-w-4xl px-4 sm:px-6 lg:px-8 mx-auto text-center\")},m(o,i){V(o,e,i),l(e,T),A.m(T,null),l(e,t),l(e,r),l(r,n)},p(o,[i]){S===(S=s(o))&&A?A.p(o,i):(A.d(1),A=S(o),A&&(A.c(),A.m(T,null))),i&1&&R!==(R=o[0].subtitle+\"\")&&Le(n,R)},i:j,o:j,d(o){o&&Y(e),A.d()}}}function eA(E,e,T){let t;return eE(E,VE,r=>T(0,t=r)),[t]}class EA extends ue{constructor(e){super(),Ce(this,e,eA,zn,_e,{})}}function tA(E){let e,T;const t=E[1].default,r=Ut(t,E,E[0],null);return{c(){e=f(\"p\"),r&&r.c(),a(e,\"class\",\"text-gray-800 dark:text-gray-200\")},m(R,n){V(R,e,n),r&&r.m(e,null),T=!0},p(R,[n]){r&&r.p&&(!T||n&1)&&ht(r,t,R,R[0],T?mt(t,R[0],n,null):Gt(R[0]),null)},i(R){T||(m(r,R),T=!0)},o(R){y(r,R),T=!1},d(R){R&&Y(e),r&&r.d(R)}}}function TA(E,e,T){let{$$slots:t={},$$scope:r}=e;return E.$$set=R=>{\"$$scope\"in R&&T(0,r=R.$$scope)},[r,t]}class aE extends ue{constructor(e){super(),Ce(this,e,TA,tA,_e,{})}}function rA(E){let e;return{c(){e=te(E[0])},m(T,t){V(T,e,t)},p(T,t){t&1&&Le(e,T[0])},d(T){T&&Y(e)}}}function RA(E){let e,T,t,r,R,n,s,S,A;s=new aE({props:{$$slots:{default:[rA]},$$scope:{ctx:E}}});const o=E[1].default,i=Ut(o,E,E[2],null);return{c(){e=f(\"li\"),T=f(\"div\"),t=f(\"div\"),r=f(\"span\"),r.innerHTML='<span class=\"text-sm font-medium text-white leading-none\">You</span>',R=$(),n=f(\"div\"),K(s.$$.fragment),S=$(),i&&i.c(),a(r,\"class\",\"flex-shrink-0 inline-flex items-center justify-center h-[2.375rem] w-[2.375rem] rounded-full bg-gray-600\"),a(n,\"class\",\"grow mt-2 space-y-3\"),a(t,\"class\",\"max-w-2xl flex gap-x-2 sm:gap-x-4\"),a(T,\"class\",\"max-w-4xl px-4 sm:px-6 lg:px-8 mx-auto\"),a(e,\"class\",\"py-2 sm:py-4\")},m(_,c){V(_,e,c),l(e,T),l(T,t),l(t,r),l(t,R),l(t,n),X(s,n,null),l(n,S),i&&i.m(n,null),A=!0},p(_,[c]){const P={};c&5&&(P.$$scope={dirty:c,ctx:_}),s.$set(P),i&&i.p&&(!A||c&4)&&ht(i,o,_,_[2],A?mt(o,_[2],c,null):Gt(_[2]),null)},i(_){A||(m(s.$$.fragment,_),m(i,_),A=!0)},o(_){y(s.$$.fragment,_),y(i,_),A=!1},d(_){_&&Y(e),k(s),i&&i.d(_)}}}function nA(E,e,T){let{$$slots:t={},$$scope:r}=e,{message:R}=e;return E.$$set=n=>{\"message\"in n&&T(0,R=n.message),\"$$scope\"in n&&T(2,r=n.$$scope)},[R,t,r]}class WE extends ue{constructor(e){super(),Ce(this,e,nA,RA,_e,{message:0})}}function AA(E){let e,T,t;return{c(){e=f(\"button\"),e.innerHTML='<svg class=\"flex-shrink-0 size-4\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z\"></path><path d=\"M19 10v2a7 7 0 0 1-14 0v-2\"></path><line x1=\"12\" x2=\"12\" y1=\"19\" y2=\"22\"></line></svg>',a(e,\"type\",\"button\"),a(e,\"class\",\"inline-flex flex-shrink-0 justify-center items-center size-8 rounded-lg text-gray-500 hover:text-blue-600 focus:z-10 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:hover:text-blue-500 dark:focus:outline-none dark:focus:ring-1 dark:focus:ring-gray-600\")},m(r,R){V(r,e,R),T||(t=Ne(e,\"click\",E[1]),T=!0)},p:j,d(r){r&&Y(e),T=!1,t()}}}function sA(E){let e;return{c(){e=f(\"button\"),e.innerHTML='<svg class=\"flex-shrink-0 size-4\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z\"></path><path d=\"M19 10v2a7 7 0 0 1-14 0v-2\"></path><line x1=\"12\" x2=\"12\" y1=\"19\" y2=\"22\"></line></svg>',a(e,\"type\",\"button\"),a(e,\"class\",\"animate-ping animate-pulse inline-flex flex-shrink-0 justify-center items-center size-8 rounded-lg text-red-500 hover:text-red-600 focus:z-10 focus:outline-none focus:ring-2 focus:ring-red-500 dark:hover:text-red-500 dark:focus:outline-none dark:focus:ring-1 dark:focus:ring-red-600\")},m(T,t){V(T,e,t)},p:j,d(T){T&&Y(e)}}}function SA(E){let e;function T(R,n){return R[0]?sA:AA}let t=T(E),r=t(E);return{c(){r.c(),e=je()},m(R,n){r.m(R,n),V(R,e,n)},p(R,[n]){t===(t=T(R))&&r?r.p(R,n):(r.d(1),r=t(R),r&&(r.c(),r.m(e.parentNode,e)))},i:j,o:j,d(R){R&&Y(e),r.d(R)}}}function oA(E,e,T){let{newMessage:t}=e,r=!1;function R(){if(T(0,r=!0),pR.set(!0),\"webkitSpeechRecognition\"in window)var n=new window.webkitSpeechRecognition;else var n=new window.SpeechRecognition;n.lang=\"en-US\",n.start(),n.onresult=s=>{const S=s.results[0][0].transcript;console.log(S),T(2,t=S),T(0,r=!1)},n.onend=()=>{T(0,r=!1)},n.onerror=()=>{T(0,r=!1)}}return E.$$set=n=>{\"newMessage\"in n&&T(2,t=n.newMessage)},[r,R,t]}class OA extends ue{constructor(e){super(),Ce(this,e,oA,SA,_e,{newMessage:2})}}function iA(E){let e,T,t,r,R,n,s,S,A,o,i,_,c,P,p;function C(I){E[5](I)}let L={};return E[0]!==void 0&&(L.newMessage=E[0]),A=new OA({props:L}),iT.push(()=>cn(A,\"newMessage\",C)),{c(){e=f(\"div\"),T=f(\"input\"),t=$(),r=f(\"div\"),R=f(\"div\"),n=f(\"div\"),n.innerHTML=\"\",s=$(),S=f(\"div\"),K(A.$$.fragment),i=$(),_=f(\"button\"),_.innerHTML='<svg class=\"h-3.5 w-3.5\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\"><path d=\"M15.964.686a.5.5 0 0 0-.65-.65L.767 5.855H.766l-.452.18a.5.5 0 0 0-.082.887l.41.26.001.002 4.995 3.178 3.178 4.995.002.002.26.41a.5.5 0 0 0 .886-.083l6-15Zm-1.833 1.89L6.637 10.07l-.215-.338a.5.5 0 0 0-.154-.154l-.338-.215 7.494-7.494 1.178-.471-.47 1.178Z\"></path></svg>',a(T,\"type\",\"text\"),a(T,\"class\",\"p-4 pb-12 block w-full bg-gray-100 border-gray-200 rounded-md text-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-slate-800 dark:border-gray-700 dark:text-gray-400\"),a(T,\"placeholder\",\"Ask me a question about your data that I can turn into SQL.\"),a(n,\"class\",\"flex items-center\"),a(_,\"type\",\"button\"),a(_,\"class\",\"inline-flex flex-shrink-0 justify-center items-center h-8 w-8 rounded-md text-white bg-blue-600 hover:bg-blue-500 focus:z-10 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all\"),a(S,\"class\",\"flex items-center gap-x-1\"),a(R,\"class\",\"flex justify-between items-center\"),a(r,\"class\",\"absolute bottom-px inset-x-px p-2 rounded-b-md bg-gray-100 dark:bg-slate-800\"),a(e,\"class\",\"relative\")},m(I,u){V(I,e,u),l(e,T),Ye(T,E[0]),l(e,t),l(e,r),l(r,R),l(R,n),l(R,s),l(R,S),X(A,S,null),l(S,i),l(S,_),c=!0,P||(p=[Ne(T,\"input\",E[4]),Ne(T,\"keydown\",E[1]),Ne(_,\"click\",E[2])],P=!0)},p(I,[u]){u&1&&T.value!==I[0]&&Ye(T,I[0]);const H={};!o&&u&1&&(o=!0,H.newMessage=I[0],ln(()=>o=!1)),A.$set(H)},i(I){c||(m(A.$$.fragment,I),c=!0)},o(I){y(A.$$.fragment,I),c=!1},d(I){I&&Y(e),k(A),P=!1,NE(p)}}}function aA(E,e,T){let t;eE(E,BE,A=>T(0,t=A));let{onSubmit:r}=e;function R(A){A.key===\"Enter\"&&(r(t),OT(BE,t=\"\",t),A.preventDefault())}function n(){r(t),OT(BE,t=\"\",t)}function s(){t=this.value,BE.set(t)}function S(A){t=A,BE.set(t)}return E.$$set=A=>{\"onSubmit\"in A&&T(3,r=A.onSubmit)},[t,R,n,r,s,S]}class IA extends ue{constructor(e){super(),Ce(this,e,aA,iA,_e,{onSubmit:3})}}function NA(E){let e;return{c(){e=f(\"div\"),e.innerHTML='<button type=\"button\" class=\"p-2 inline-flex justify-center items-center gap-1.5 rounded-md border font-medium bg-white text-gray-700 shadow-sm align-middle hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-white focus:ring-blue-600 transition-all text-xs dark:bg-slate-900 dark:hover:bg-slate-800 dark:border-gray-700 dark:text-gray-400 dark:hover:text-white dark:focus:ring-offset-gray-800\" data-hs-overlay=\"#application-sidebar\" aria-controls=\"application-sidebar\" aria-label=\"Toggle navigation\"><svg class=\"w-3.5 h-3.5\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z\"></path></svg> <span>Sidebar</span></button>',a(e,\"class\",\"lg:hidden flex justify-end mb-2 sm:mb-3\")},m(T,t){V(T,e,t)},p:j,i:j,o:j,d(T){T&&Y(e)}}}class lA extends ue{constructor(e){super(),Ce(this,e,null,NA,_e,{})}}function _A(E){let e,T,t,r;return{c(){e=f(\"button\"),T=te(E[0]),a(e,\"type\",\"button\"),a(e,\"class\",\"mb-2.5 mr-1.5 py-2 px-3 inline-flex justify-center items-center gap-x-2 rounded-md border border-blue-600 bg-white text-blue-600 align-middle hover:bg-blue-50 text-sm dark:bg-slate-900 dark:text-blue-500 dark:border-blue-500 dark:hover:text-blue-400 dark:hover:border-blue-400\")},m(R,n){V(R,e,n),l(e,T),t||(r=Ne(e,\"click\",E[1]),t=!0)},p(R,[n]){n&1&&Le(T,R[0])},i:j,o:j,d(R){R&&Y(e),t=!1,r()}}}function LA(E,e,T){let{message:t}=e,{onSubmit:r}=e;function R(){r(t)}return E.$$set=n=>{\"message\"in n&&T(0,t=n.message),\"onSubmit\"in n&&T(2,r=n.onSubmit)},[t,R,r]}class IE extends ue{constructor(e){super(),Ce(this,e,LA,_A,_e,{message:0,onSubmit:2})}}function CA(E){let e,T,t,r,R,n,s,S,A,o,i;return{c(){e=f(\"span\"),T=OE(\"svg\"),t=OE(\"defs\"),r=OE(\"linearGradient\"),R=OE(\"stop\"),n=OE(\"stop\"),s=OE(\"g\"),S=OE(\"g\"),A=OE(\"path\"),o=OE(\"path\"),a(R,\"offset\",\"0\"),a(R,\"stop-color\",\"#009efd\"),a(n,\"offset\",\"1\"),a(n,\"stop-color\",\"#2af598\"),a(r,\"gradientTransform\",\"matrix(1.09331 0 0 1.09331 -47.1838 -88.8946)\"),a(r,\"gradientUnits\",\"userSpaceOnUse\"),a(r,\"id\",\"LinearGradient\"),a(r,\"x1\",\"237.82\"),a(r,\"x2\",\"785.097\"),a(r,\"y1\",\"549.609\"),a(r,\"y2\",\"549.609\"),a(A,\"d\",\"M117.718 228.798C117.718 119.455 206.358 30.8151 315.701 30.8151L708.299 30.8151C817.642 30.8151 906.282 119.455 906.282 228.798L906.282 795.202C906.282 904.545 817.642 993.185 708.299 993.185L315.701 993.185C206.358 993.185 117.718 904.545 117.718 795.202L117.718 228.798Z\"),a(A,\"fill\",\"#0f172a\"),a(A,\"fill-rule\",\"nonzero\"),a(A,\"opacity\",\"1\"),a(A,\"stroke\",\"#374151\"),a(A,\"stroke-linecap\",\"butt\"),a(A,\"stroke-linejoin\",\"round\"),a(A,\"stroke-width\",\"20\"),a(o,\"d\",\"M212.828 215.239C213.095 281.169 213.629 413.028 213.629 413.028C213.629 413.028 511.51 808.257 513.993 809.681C612.915 677.809 810.759 414.065 810.759 414.065C810.759 414.065 811.034 280.901 811.172 214.319C662.105 362.973 662.105 362.973 513.038 511.627C362.933 363.433 362.933 363.433 212.828 215.239Z\"),a(o,\"fill\",\"url(#LinearGradient)\"),a(o,\"fill-rule\",\"nonzero\"),a(o,\"opacity\",\"1\"),a(o,\"stroke\",\"none\"),a(S,\"opacity\",\"1\"),a(s,\"id\",\"Layer-1\"),a(T,\"height\",\"100%\"),a(T,\"stroke-miterlimit\",\"10\"),ct(T,\"fill-rule\",\"nonzero\"),ct(T,\"clip-rule\",\"evenodd\"),ct(T,\"stroke-linecap\",\"round\"),ct(T,\"stroke-linejoin\",\"round\"),a(T,\"version\",\"1.1\"),a(T,\"viewBox\",\"0 0 1024 1024\"),a(T,\"width\",\"100%\"),a(T,\"xml:space\",\"preserve\"),a(T,\"xmlns\",\"http://www.w3.org/2000/svg\"),a(e,\"class\",i=\"flex-shrink-0 w-[2.375rem] h-[2.375rem] \"+E[0])},m(_,c){V(_,e,c),l(e,T),l(T,t),l(t,r),l(r,R),l(r,n),l(T,s),l(s,S),l(S,A),l(S,o)},p(_,[c]){c&1&&i!==(i=\"flex-shrink-0 w-[2.375rem] h-[2.375rem] \"+_[0])&&a(e,\"class\",i)},i:j,o:j,d(_){_&&Y(e)}}}function uA(E,e,T){let t,{animate:r=!1}=e;return E.$$set=R=>{\"animate\"in R&&T(1,r=R.animate)},E.$$.update=()=>{E.$$.dirty&2&&T(0,t=r?\"animate-bounce\":\"\")},[t,r]}class GR extends ue{constructor(e){super(),Ce(this,e,uA,CA,_e,{animate:1})}}function cA(E){let e,T,t,r,R;T=new GR({});const n=E[1].default,s=Ut(n,E,E[0],null);return{c(){e=f(\"li\"),K(T.$$.fragment),t=$(),r=f(\"div\"),s&&s.c(),a(r,\"class\",\"space-y-3 overflow-x-auto overflow-y-hidden whitespace-break-spaces w-full\"),a(e,\"class\",\"max-w-4xl py-2 px-4 sm:px-6 lg:px-8 mx-auto flex gap-x-2 sm:gap-x-4\")},m(S,A){V(S,e,A),X(T,e,null),l(e,t),l(e,r),s&&s.m(r,null),R=!0},p(S,[A]){s&&s.p&&(!R||A&1)&&ht(s,n,S,S[0],R?mt(n,S[0],A,null):Gt(S[0]),null)},i(S){R||(m(T.$$.fragment,S),m(s,S),R=!0)},o(S){y(T.$$.fragment,S),y(s,S),R=!1},d(S){S&&Y(e),k(T),s&&s.d(S)}}}function fA(E,e,T){let{$$slots:t={},$$scope:r}=e;return E.$$set=R=>{\"$$scope\"in R&&T(0,r=R.$$scope)},[r,t]}class Ze extends ue{constructor(e){super(),Ce(this,e,fA,cA,_e,{})}}function PA(E){let e;return{c(){e=te(\"Thinking...\")},m(T,t){V(T,e,t)},d(T){T&&Y(e)}}}function DA(E){let e,T,t,r,R,n;return T=new GR({props:{animate:!0}}),R=new aE({props:{$$slots:{default:[PA]},$$scope:{ctx:E}}}),{c(){e=f(\"li\"),K(T.$$.fragment),t=$(),r=f(\"div\"),K(R.$$.fragment),a(r,\"class\",\"space-y-3\"),a(e,\"class\",\"max-w-4xl py-2 px-4 sm:px-6 lg:px-8 mx-auto flex gap-x-2 sm:gap-x-4\")},m(s,S){V(s,e,S),X(T,e,null),l(e,t),l(e,r),X(R,r,null),n=!0},p(s,[S]){const A={};S&1&&(A.$$scope={dirty:S,ctx:s}),R.$set(A)},i(s){n||(m(T.$$.fragment,s),m(R.$$.fragment,s),n=!0)},o(s){y(T.$$.fragment,s),y(R.$$.fragment,s),n=!1},d(s){s&&Y(e),k(T),k(R)}}}class dA extends ue{constructor(e){super(),Ce(this,e,null,DA,_e,{})}}function pA(E){let e,T,t,r,R,n,s,S,A,o,i;return{c(){e=f(\"ul\"),T=f(\"li\"),t=f(\"div\"),r=f(\"span\"),r.textContent=\"CSV\",R=$(),n=f(\"a\"),s=OE(\"svg\"),S=OE(\"path\"),A=OE(\"path\"),o=te(`\n          Download`),a(r,\"class\",\"mr-3 flex-1 w-0 truncate\"),a(S,\"d\",\"M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z\"),a(A,\"d\",\"M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z\"),a(s,\"class\",\"flex-shrink-0 w-3 h-3\"),a(s,\"width\",\"16\"),a(s,\"height\",\"16\"),a(s,\"viewBox\",\"0 0 16 16\"),a(s,\"fill\",\"currentColor\"),a(n,\"class\",\"flex items-center gap-x-2 text-gray-500 hover:text-blue-500 whitespace-nowrap\"),a(n,\"href\",i=\"/api/v0/download_csv?id=\"+E[0]),a(t,\"class\",\"w-full flex justify-between truncate\"),a(T,\"class\",\"flex items-center gap-x-2 p-3 text-sm bg-white border text-gray-800 first:rounded-t-lg first:mt-0 last:rounded-b-lg dark:bg-slate-900 dark:border-gray-700 dark:text-gray-200\"),a(e,\"class\",\"flex flex-col justify-end text-start -space-y-px\")},m(_,c){V(_,e,c),l(e,T),l(T,t),l(t,r),l(t,R),l(t,n),l(n,s),l(s,S),l(s,A),l(n,o)},p(_,[c]){c&1&&i!==(i=\"/api/v0/download_csv?id=\"+_[0])&&a(n,\"href\",i)},i:j,o:j,d(_){_&&Y(e)}}}function MA(E,e,T){let{id:t}=e;return E.$$set=r=>{\"id\"in r&&T(0,t=r.id)},[t]}class UA extends ue{constructor(e){super(),Ce(this,e,MA,pA,_e,{id:0})}}function KT(E,e,T){const t=E.slice();return t[5]=e[T],t}function JT(E,e,T){const t=E.slice();return t[8]=e[T],t}function qT(E,e,T){const t=E.slice();return t[8]=e[T],t}function QT(E){let e,T,t,r;return{c(){e=f(\"th\"),T=f(\"div\"),t=f(\"span\"),t.textContent=`${E[8]}`,r=$(),a(t,\"class\",\"text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-gray-200\"),a(T,\"class\",\"flex items-center gap-x-2\"),a(e,\"scope\",\"col\"),a(e,\"class\",\"px-6 py-3 text-left\")},m(R,n){V(R,e,n),l(e,T),l(T,t),l(e,r)},p:j,d(R){R&&Y(e)}}}function ZT(E){let e,T,t;return{c(){e=f(\"td\"),T=f(\"div\"),t=f(\"span\"),t.textContent=`${E[5][E[8]]}`,a(t,\"class\",\"text-gray-800 dark:text-gray-200\"),a(T,\"class\",\"px-6 py-3\"),a(e,\"class\",\"h-px w-px whitespace-nowrap\")},m(r,R){V(r,e,R),l(e,T),l(T,t)},p:j,d(r){r&&Y(e)}}}function jT(E){let e,T,t=De(E[3]),r=[];for(let R=0;R<t.length;R+=1)r[R]=ZT(JT(E,t,R));return{c(){e=f(\"tr\");for(let R=0;R<r.length;R+=1)r[R].c();T=$()},m(R,n){V(R,e,n);for(let s=0;s<r.length;s+=1)r[s]&&r[s].m(e,null);l(e,T)},p(R,n){if(n&12){t=De(R[3]);let s;for(s=0;s<t.length;s+=1){const S=JT(R,t,s);r[s]?r[s].p(S,n):(r[s]=ZT(S),r[s].c(),r[s].m(e,T))}for(;s<r.length;s+=1)r[s].d(1);r.length=t.length}},d(R){R&&Y(e),nE(r,R)}}}function zT(E){let e,T;return e=new UA({props:{id:E[0]}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&1&&(R.id=t[0]),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function mA(E){let e,T,t,r,R,n,s,S,A,o,i,_=De(E[3]),c=[];for(let L=0;L<_.length;L+=1)c[L]=QT(qT(E,_,L));let P=De(E[2]),p=[];for(let L=0;L<P.length;L+=1)p[L]=jT(KT(E,P,L));let C=E[1].csv_download&&zT(E);return{c(){e=f(\"div\"),T=f(\"div\"),t=f(\"div\"),r=f(\"table\"),R=f(\"thead\"),n=f(\"tr\");for(let L=0;L<c.length;L+=1)c[L].c();s=$(),S=f(\"tbody\");for(let L=0;L<p.length;L+=1)p[L].c();A=$(),C&&C.c(),o=je(),a(R,\"class\",\"bg-gray-50 dark:bg-slate-800\"),a(S,\"class\",\"divide-y divide-gray-200 dark:divide-gray-700\"),a(r,\"class\",\"min-w-full divide-y divide-gray-200 dark:divide-gray-700\"),a(t,\"class\",\"p-1.5 min-w-full inline-block align-middle\"),a(T,\"class\",\"-m-1.5 overflow-x-auto\"),a(e,\"class\",\"bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden dark:bg-slate-900 dark:border-gray-700\")},m(L,I){V(L,e,I),l(e,T),l(T,t),l(t,r),l(r,R),l(R,n);for(let u=0;u<c.length;u+=1)c[u]&&c[u].m(n,null);l(r,s),l(r,S);for(let u=0;u<p.length;u+=1)p[u]&&p[u].m(S,null);V(L,A,I),C&&C.m(L,I),V(L,o,I),i=!0},p(L,[I]){if(I&8){_=De(L[3]);let u;for(u=0;u<_.length;u+=1){const H=qT(L,_,u);c[u]?c[u].p(H,I):(c[u]=QT(H),c[u].c(),c[u].m(n,null))}for(;u<c.length;u+=1)c[u].d(1);c.length=_.length}if(I&12){P=De(L[2]);let u;for(u=0;u<P.length;u+=1){const H=KT(L,P,u);p[u]?p[u].p(H,I):(p[u]=jT(H),p[u].c(),p[u].m(S,null))}for(;u<p.length;u+=1)p[u].d(1);p.length=P.length}L[1].csv_download?C?(C.p(L,I),I&2&&m(C,1)):(C=zT(L),C.c(),m(C,1),C.m(o.parentNode,o)):C&&(Ge(),y(C,1,1,()=>{C=null}),ge())},i(L){i||(m(C),i=!0)},o(L){y(C),i=!1},d(L){L&&(Y(e),Y(A),Y(o)),nE(c,L),nE(p,L),C&&C.d(L)}}}function hA(E,e,T){let t;eE(E,VE,S=>T(1,t=S));let{id:r}=e,{df:R}=e,n=JSON.parse(R),s=n.length>0?Object.keys(n[0]):[];return E.$$set=S=>{\"id\"in S&&T(0,r=S.id),\"df\"in S&&T(4,R=S.df)},[r,t,n,s,R]}class gR extends ue{constructor(e){super(),Ce(this,e,hA,mA,_e,{id:0,df:4})}}function GA(E){let e;return{c(){e=f(\"div\"),a(e,\"id\",E[0])},m(T,t){V(T,e,t)},p:j,i:j,o:j,d(T){T&&Y(e)}}}function gA(E,e,T){let{fig:t}=e,r=JSON.parse(t),R=Math.random().toString(36).substring(2,15)+Math.random().toString(36).substring(2,15);return DR(()=>{Plotly.newPlot(document.getElementById(R),r,{responsive:!0})}),E.$$set=n=>{\"fig\"in n&&T(1,t=n.fig)},[R,t]}class HR extends ue{constructor(e){super(),Ce(this,e,gA,GA,_e,{fig:1})}}function HA(E){let e,T,t,r;return{c(){e=f(\"button\"),T=te(E[0]),a(e,\"type\",\"button\"),a(e,\"class\",\"mb-2.5 mr-1.5 py-3 px-4 inline-flex justify-center items-center gap-2 rounded-md border-2 border-green-200 font-semibold text-green-500 hover:text-white hover:bg-green-500 hover:border-green-500 focus:outline-none focus:ring-2 focus:ring-green-200 focus:ring-offset-2 transition-all text-sm dark:focus:ring-offset-gray-800\")},m(R,n){V(R,e,n),l(e,T),t||(r=Ne(e,\"click\",E[1]),t=!0)},p(R,[n]){n&1&&Le(T,R[0])},i:j,o:j,d(R){R&&Y(e),t=!1,r()}}}function bA(E,e,T){let{message:t}=e,{onSubmit:r}=e;function R(){r(t)}return E.$$set=n=>{\"message\"in n&&T(0,t=n.message),\"onSubmit\"in n&&T(2,r=n.onSubmit)},[t,R,r]}class yA extends ue{constructor(e){super(),Ce(this,e,bA,HA,_e,{message:0,onSubmit:2})}}function BA(E){let e,T,t,r,R,n,s,S,A;return{c(){e=f(\"div\"),T=f(\"div\"),t=f(\"div\"),t.innerHTML='<svg class=\"h-4 w-4 text-yellow-400 mt-0.5\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\"><path d=\"M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z\"></path></svg>',r=$(),R=f(\"div\"),n=f(\"h3\"),n.textContent=\"Error\",s=$(),S=f(\"div\"),A=te(E[0]),a(t,\"class\",\"flex-shrink-0\"),a(n,\"class\",\"text-sm text-yellow-800 font-semibold\"),a(S,\"class\",\"mt-1 text-sm text-yellow-700\"),a(R,\"class\",\"ml-4\"),a(T,\"class\",\"flex\"),a(e,\"class\",\"bg-yellow-50 border border-yellow-200 rounded-md p-4\"),a(e,\"role\",\"alert\")},m(o,i){V(o,e,i),l(e,T),l(T,t),l(T,r),l(T,R),l(R,n),l(R,s),l(R,S),l(S,A)},p(o,[i]){i&1&&Le(A,o[0])},i:j,o:j,d(o){o&&Y(e)}}}function vA(E,e,T){let{message:t}=e;return E.$$set=r=>{\"message\"in r&&T(0,t=r.message)},[t]}let PT=class extends ue{constructor(e){super(),Ce(this,e,vA,BA,_e,{message:0})}};function FA(E){let e,T;const t=E[1].default,r=Ut(t,E,E[0],null);return{c(){e=f(\"div\"),r&&r.c(),a(e,\"class\",\"font-mono whitespace-pre-wrap\")},m(R,n){V(R,e,n),r&&r.m(e,null),T=!0},p(R,[n]){r&&r.p&&(!T||n&1)&&ht(r,t,R,R[0],T?mt(t,R[0],n,null):Gt(R[0]),null)},i(R){T||(m(r,R),T=!0)},o(R){y(r,R),T=!1},d(R){R&&Y(e),r&&r.d(R)}}}function YA(E,e,T){let{$$slots:t={},$$scope:r}=e;return E.$$set=R=>{\"$$scope\"in R&&T(0,r=R.$$scope)},[r,t]}class bR extends ue{constructor(e){super(),Ce(this,e,YA,FA,_e,{})}}function VA(E){let e;return{c(){e=te(E[1])},m(T,t){V(T,e,t)},p(T,t){t&2&&Le(e,T[1])},d(T){T&&Y(e)}}}function WA(E){let e,T,t,r,R,n,s,S;return t=new IE({props:{message:\"Run SQL\",onSubmit:E[3]}}),R=new aE({props:{$$slots:{default:[VA]},$$scope:{ctx:E}}}),{c(){e=f(\"textarea\"),T=$(),K(t.$$.fragment),r=$(),K(R.$$.fragment),a(e,\"rows\",\"6\"),a(e,\"class\",\"block p-2.5 w-full text-blue-600 hover:text-blue-500 dark:text-blue-500 dark:hover:text-blue-400 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 font-mono\"),a(e,\"placeholder\",\"SELECT col1, col2, col3 FROM ...\")},m(A,o){V(A,e,o),Ye(e,E[1]),V(A,T,o),X(t,A,o),V(A,r,o),X(R,A,o),n=!0,s||(S=Ne(e,\"input\",E[2]),s=!0)},p(A,[o]){o&2&&Ye(e,A[1]);const i={};o&3&&(i.onSubmit=A[3]),t.$set(i);const _={};o&18&&(_.$$scope={dirty:o,ctx:A}),R.$set(_)},i(A){n||(m(t.$$.fragment,A),m(R.$$.fragment,A),n=!0)},o(A){y(t.$$.fragment,A),y(R.$$.fragment,A),n=!1},d(A){A&&(Y(e),Y(T),Y(r)),k(t,A),k(R,A),s=!1,S()}}}function wA(E,e,T){let t;eE(E,Et,s=>T(1,t=s));let{onSubmit:r}=e;function R(){t=this.value,Et.set(t)}const n=()=>r(t);return E.$$set=s=>{\"onSubmit\"in s&&T(0,r=s.onSubmit)},[r,t,R,n]}class $A extends ue{constructor(e){super(),Ce(this,e,wA,WA,_e,{onSubmit:0})}}function xA(E){let e,T,t,r,R,n;return t=new IE({props:{message:E[3],onSubmit:E[5]}}),{c(){e=f(\"textarea\"),T=$(),K(t.$$.fragment),a(e,\"rows\",\"6\"),a(e,\"class\",\"block p-2.5 w-full text-blue-600 hover:text-blue-500 dark:text-blue-500 dark:hover:text-blue-400 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 font-mono\"),a(e,\"placeholder\",E[2])},m(s,S){V(s,e,S),Ye(e,E[0]),V(s,T,S),X(t,s,S),r=!0,R||(n=Ne(e,\"input\",E[4]),R=!0)},p(s,[S]){(!r||S&4)&&a(e,\"placeholder\",s[2]),S&1&&Ye(e,s[0]);const A={};S&8&&(A.message=s[3]),S&3&&(A.onSubmit=s[5]),t.$set(A)},i(s){r||(m(t.$$.fragment,s),r=!0)},o(s){y(t.$$.fragment,s),r=!1},d(s){s&&(Y(e),Y(T)),k(t,s),R=!1,n()}}}function XA(E,e,T){let{onSubmit:t}=e,{currentValue:r}=e,{placeholder:R}=e,{buttonText:n}=e;function s(){r=this.value,T(0,r)}const S=()=>t(r);return E.$$set=A=>{\"onSubmit\"in A&&T(1,t=A.onSubmit),\"currentValue\"in A&&T(0,r=A.currentValue),\"placeholder\"in A&&T(2,R=A.placeholder),\"buttonText\"in A&&T(3,n=A.buttonText)},[r,t,R,n,s,S]}class kA extends ue{constructor(e){super(),Ce(this,e,XA,xA,_e,{onSubmit:1,currentValue:0,placeholder:2,buttonText:3})}}function KA(E){let e,T;return e=new IE({props:{message:\"Play\",onSubmit:E[2]}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,[r]){const R={};r&1&&(R.onSubmit=t[2]),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function er(E){if(\"speechSynthesis\"in window){const e=new SpeechSynthesisUtterance(E);e.lang=\"en-US\",e.volume=1,e.rate=1,e.pitch=1,window.speechSynthesis.speak(e)}else console.error(\"SpeechSynthesis API is not supported in this browser.\")}function JA(E,e,T){let t;eE(E,pR,n=>T(1,t=n));let{message:r}=e;const R=()=>er(r);return E.$$set=n=>{\"message\"in n&&T(0,r=n.message)},E.$$.update=()=>{E.$$.dirty&3&&t&&er(r)},[r,t,R]}class qA extends ue{constructor(e){super(),Ce(this,e,JA,KA,_e,{message:0})}}function QA(E){let e,T,t;return{c(){e=f(\"button\"),e.textContent=\"Open Debugger\",T=$(),t=f(\"div\"),t.innerHTML='<div class=\"flex justify-between items-center py-3 px-4 border-b dark:border-neutral-700\"><h3 class=\"font-bold text-gray-800 dark:text-white\">Server Logs</h3> <button type=\"button\" class=\"flex justify-center items-center size-7 text-sm font-semibold rounded-full border border-transparent text-gray-800 hover:bg-gray-100 disabled:opacity-50 disabled:pointer-events-none dark:text-white dark:hover:bg-neutral-700\" data-hs-overlay=\"#hs-overlay-right\"><span class=\"sr-only\">Close modal</span> <svg class=\"flex-shrink-0 size-4\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18 6 6 18\"></path><path d=\"m6 6 12 12\"></path></svg></button></div> <div class=\"p-4\"><p id=\"log-contents\" class=\"text-gray-800 dark:text-neutral-400\"></p></div>',a(e,\"type\",\"button\"),a(e,\"class\",\"absolute top-0 right-0 m-1 ms-0 py-3 px-4 inline-flex items-center gap-x-2 text-sm font-semibold rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none\"),a(e,\"data-hs-overlay\",\"#hs-overlay-right\"),a(t,\"id\",\"hs-overlay-right\"),a(t,\"class\",\"hs-overlay hs-overlay-open:translate-x-0 hidden translate-x-full fixed top-0 end-0 transition-all duration-300 transform h-full max-w-xs w-full z-[80] bg-white border-s dark:bg-neutral-800 dark:border-neutral-700 [--body-scroll:true] overflow-y-auto\"),a(t,\"tabindex\",\"-1\")},m(r,R){V(r,e,R),V(r,T,R),V(r,t,R)},p:j,i:j,o:j,d(r){r&&(Y(e),Y(T),Y(t))}}}class ZA extends ue{constructor(e){super(),Ce(this,e,null,QA,_e,{})}}var yR={exports:{}};(function(E){(function(e,T){E.exports?E.exports=T():e.nearley=T()})(Jn,function(){function e(A,o,i){return this.id=++e.highestId,this.name=A,this.symbols=o,this.postprocess=i,this}e.highestId=0,e.prototype.toString=function(A){var o=typeof A>\"u\"?this.symbols.map(S).join(\" \"):this.symbols.slice(0,A).map(S).join(\" \")+\" ● \"+this.symbols.slice(A).map(S).join(\" \");return this.name+\" → \"+o};function T(A,o,i,_){this.rule=A,this.dot=o,this.reference=i,this.data=[],this.wantedBy=_,this.isComplete=this.dot===A.symbols.length}T.prototype.toString=function(){return\"{\"+this.rule.toString(this.dot)+\"}, from: \"+(this.reference||0)},T.prototype.nextState=function(A){var o=new T(this.rule,this.dot+1,this.reference,this.wantedBy);return o.left=this,o.right=A,o.isComplete&&(o.data=o.build(),o.right=void 0),o},T.prototype.build=function(){var A=[],o=this;do A.push(o.right.data),o=o.left;while(o.left);return A.reverse(),A},T.prototype.finish=function(){this.rule.postprocess&&(this.data=this.rule.postprocess(this.data,this.reference,n.fail))};function t(A,o){this.grammar=A,this.index=o,this.states=[],this.wants={},this.scannable=[],this.completed={}}t.prototype.process=function(A){for(var o=this.states,i=this.wants,_=this.completed,c=0;c<o.length;c++){var P=o[c];if(P.isComplete){if(P.finish(),P.data!==n.fail){for(var p=P.wantedBy,C=p.length;C--;){var L=p[C];this.complete(L,P)}if(P.reference===this.index){var I=P.rule.name;(this.completed[I]=this.completed[I]||[]).push(P)}}}else{var I=P.rule.symbols[P.dot];if(typeof I!=\"string\"){this.scannable.push(P);continue}if(i[I]){if(i[I].push(P),_.hasOwnProperty(I))for(var u=_[I],C=0;C<u.length;C++){var H=u[C];this.complete(P,H)}}else i[I]=[P],this.predict(I)}}},t.prototype.predict=function(A){for(var o=this.grammar.byName[A]||[],i=0;i<o.length;i++){var _=o[i],c=this.wants[A],P=new T(_,0,this.index,c);this.states.push(P)}},t.prototype.complete=function(A,o){var i=A.nextState(o);this.states.push(i)};function r(A,o){this.rules=A,this.start=o||this.rules[0].name;var i=this.byName={};this.rules.forEach(function(_){i.hasOwnProperty(_.name)||(i[_.name]=[]),i[_.name].push(_)})}r.fromCompiled=function(_,o){var i=_.Lexer;_.ParserStart&&(o=_.ParserStart,_=_.ParserRules);var _=_.map(function(P){return new e(P.name,P.symbols,P.postprocess)}),c=new r(_,o);return c.lexer=i,c};function R(){this.reset(\"\")}R.prototype.reset=function(A,o){this.buffer=A,this.index=0,this.line=o?o.line:1,this.lastLineBreak=o?-o.col:0},R.prototype.next=function(){if(this.index<this.buffer.length){var A=this.buffer[this.index++];return A===`\n`&&(this.line+=1,this.lastLineBreak=this.index),{value:A}}},R.prototype.save=function(){return{line:this.line,col:this.index-this.lastLineBreak}},R.prototype.formatError=function(A,o){var i=this.buffer;if(typeof i==\"string\"){var _=i.split(`\n`).slice(Math.max(0,this.line-5),this.line),c=i.indexOf(`\n`,this.index);c===-1&&(c=i.length);var P=this.index-this.lastLineBreak,p=String(this.line).length;return o+=\" at line \"+this.line+\" col \"+P+`:\n\n`,o+=_.map(function(L,I){return C(this.line-_.length+I+1,p)+\" \"+L},this).join(`\n`),o+=`\n`+C(\"\",p+P)+`^\n`,o}else return o+\" at index \"+(this.index-1);function C(L,I){var u=String(L);return Array(I-u.length+1).join(\" \")+u}};function n(A,o,i){if(A instanceof r)var _=A,i=o;else var _=r.fromCompiled(A,o);this.grammar=_,this.options={keepHistory:!1,lexer:_.lexer||new R};for(var c in i||{})this.options[c]=i[c];this.lexer=this.options.lexer,this.lexerState=void 0;var P=new t(_,0);this.table=[P],P.wants[_.start]=[],P.predict(_.start),P.process(),this.current=0}n.fail={},n.prototype.feed=function(A){var o=this.lexer;o.reset(A,this.lexerState);for(var i;;){try{if(i=o.next(),!i)break}catch(O){var p=new t(this.grammar,this.current+1);this.table.push(p);var _=new Error(this.reportLexerError(O));throw _.offset=this.current,_.token=O.token,_}var c=this.table[this.current];this.options.keepHistory||delete this.table[this.current-1];var P=this.current+1,p=new t(this.grammar,P);this.table.push(p);for(var C=i.text!==void 0?i.text:i.value,L=o.constructor===R?i.value:i,I=c.scannable,u=I.length;u--;){var H=I[u],b=H.rule.symbols[H.dot];if(b.test?b.test(L):b.type?b.type===i.type:b.literal===C){var M=H.nextState({data:L,token:i,isToken:!0,reference:P-1});p.states.push(M)}}if(p.process(),p.states.length===0){var _=new Error(this.reportError(i));throw _.offset=this.current,_.token=i,_}this.options.keepHistory&&(c.lexerState=o.save()),this.current++}return c&&(this.lexerState=o.save()),this.results=this.finish(),this},n.prototype.reportLexerError=function(A){var o,i,_=A.token;return _?(o=\"input \"+JSON.stringify(_.text[0])+\" (lexer error)\",i=this.lexer.formatError(_,\"Syntax error\")):(o=\"input (lexer error)\",i=A.message),this.reportErrorCommon(i,o)},n.prototype.reportError=function(A){var o=(A.type?A.type+\" token: \":\"\")+JSON.stringify(A.value!==void 0?A.value:A),i=this.lexer.formatError(A,\"Syntax error\");return this.reportErrorCommon(i,o)},n.prototype.reportErrorCommon=function(A,o){var i=[];i.push(A);var _=this.table.length-2,c=this.table[_],P=c.states.filter(function(C){var L=C.rule.symbols[C.dot];return L&&typeof L!=\"string\"});if(P.length===0)i.push(\"Unexpected \"+o+`. I did not expect any more input. Here is the state of my parse table:\n`),this.displayStateStack(c.states,i);else{i.push(\"Unexpected \"+o+`. Instead, I was expecting to see one of the following:\n`);var p=P.map(function(C){return this.buildFirstStateStack(C,[])||[C]},this);p.forEach(function(C){var L=C[0],I=L.rule.symbols[L.dot],u=this.getSymbolDisplay(I);i.push(\"A \"+u+\" based on:\"),this.displayStateStack(C,i)},this)}return i.push(\"\"),i.join(`\n`)},n.prototype.displayStateStack=function(A,o){for(var i,_=0,c=0;c<A.length;c++){var P=A[c],p=P.rule.toString(P.dot);p===i?_++:(_>0&&o.push(\"    ^ \"+_+\" more lines identical to this\"),_=0,o.push(\"    \"+p)),i=p}},n.prototype.getSymbolDisplay=function(A){return s(A)},n.prototype.buildFirstStateStack=function(A,o){if(o.indexOf(A)!==-1)return null;if(A.wantedBy.length===0)return[A];var i=A.wantedBy[0],_=[A].concat(o),c=this.buildFirstStateStack(i,_);return c===null?null:[A].concat(c)},n.prototype.save=function(){var A=this.table[this.current];return A.lexerState=this.lexerState,A},n.prototype.restore=function(A){var o=A.index;this.current=o,this.table[o]=A,this.table.splice(o+1),this.lexerState=A.lexerState,this.results=this.finish()},n.prototype.rewind=function(A){if(!this.options.keepHistory)throw new Error(\"set option `keepHistory` to enable rewinding\");this.restore(this.table[A])},n.prototype.finish=function(){var A=[],o=this.grammar.start,i=this.table[this.table.length-1];return i.states.forEach(function(_){_.rule.name===o&&_.dot===_.rule.symbols.length&&_.reference===0&&_.data!==n.fail&&A.push(_)}),A.map(function(_){return _.data})};function s(A){var o=typeof A;if(o===\"string\")return A;if(o===\"object\"){if(A.literal)return JSON.stringify(A.literal);if(A instanceof RegExp)return\"character matching \"+A;if(A.type)return A.type+\" token\";if(A.test)return\"token matching \"+String(A.test);throw new Error(\"Unknown symbol type: \"+A)}}function S(A){var o=typeof A;if(o===\"string\")return A;if(o===\"object\"){if(A.literal)return JSON.stringify(A.literal);if(A instanceof RegExp)return A.toString();if(A.type)return\"%\"+A.type;if(A.test)return\"<\"+String(A.test)+\">\";throw new Error(\"Unknown symbol type: \"+A)}}return{Parser:n,Grammar:r,Rule:e}})})(yR);var jA=yR.exports;const zA=qn(jA);var BR=Object.defineProperty,es=Object.defineProperties,Es=Object.getOwnPropertyDescriptors,Mt=Object.getOwnPropertySymbols,vR=Object.prototype.hasOwnProperty,FR=Object.prototype.propertyIsEnumerable,Er=(E,e,T)=>e in E?BR(E,e,{enumerable:!0,configurable:!0,writable:!0,value:T}):E[e]=T,EE=(E,e)=>{for(var T in e||(e={}))vR.call(e,T)&&Er(E,T,e[T]);if(Mt)for(var T of Mt(e))FR.call(e,T)&&Er(E,T,e[T]);return E},AE=(E,e)=>es(E,Es(e)),ts=(E,e)=>{var T={};for(var t in E)vR.call(E,t)&&e.indexOf(t)<0&&(T[t]=E[t]);if(E!=null&&Mt)for(var t of Mt(E))e.indexOf(t)<0&&FR.call(E,t)&&(T[t]=E[t]);return T},Ts=(E,e)=>{for(var T in e)BR(E,T,{get:e[T],enumerable:!0})},YR={};Ts(YR,{bigquery:()=>cs,db2:()=>bs,db2i:()=>$s,hive:()=>js,mariadb:()=>AS,mysql:()=>lS,n1ql:()=>bS,plsql:()=>$S,postgresql:()=>zS,redshift:()=>so,singlestoredb:()=>oO,snowflake:()=>CO,spark:()=>_o,sql:()=>vo,sqlite:()=>Uo,tidb:()=>dS,transactsql:()=>EO,trino:()=>ko});var v=E=>E.flatMap(rs),rs=E=>Pt(ns(E)).map(Rs),Rs=E=>E.replace(/ +/g,\" \").trim(),ns=E=>({type:\"mandatory_block\",items:DT(E,0)[0]}),DT=(E,e,T)=>{const t=[];for(;E[e];){const[r,R]=As(E,e);if(t.push(r),e=R,E[e]===\"|\")e++;else if(E[e]===\"}\"||E[e]===\"]\"){if(T!==E[e])throw new Error(`Unbalanced parenthesis in: ${E}`);return e++,[t,e]}else if(e===E.length){if(T)throw new Error(`Unbalanced parenthesis in: ${E}`);return[t,e]}else throw new Error(`Unexpected \"${E[e]}\"`)}return[t,e]},As=(E,e)=>{const T=[];for(;;){const[t,r]=ss(E,e);if(t)T.push(t),e=r;else break}return T.length===1?[T[0],e]:[{type:\"concatenation\",items:T},e]},ss=(E,e)=>{if(E[e]===\"{\")return Ss(E,e+1);if(E[e]===\"[\")return os(E,e+1);{let T=\"\";for(;E[e]&&/[A-Za-z0-9_ ]/.test(E[e]);)T+=E[e],e++;return[T,e]}},Ss=(E,e)=>{const[T,t]=DT(E,e,\"}\");return[{type:\"mandatory_block\",items:T},t]},os=(E,e)=>{const[T,t]=DT(E,e,\"]\");return[{type:\"optional_block\",items:T},t]},Pt=E=>{if(typeof E==\"string\")return[E];if(E.type===\"concatenation\")return E.items.map(Pt).reduce(Os,[\"\"]);if(E.type===\"mandatory_block\")return E.items.flatMap(Pt);if(E.type===\"optional_block\")return[\"\",...E.items.flatMap(Pt)];throw new Error(`Unknown node type: ${E}`)},Os=(E,e)=>{const T=[];for(const t of E)for(const r of e)T.push(t+r);return T},VR=(E=>(E.QUOTED_IDENTIFIER=\"QUOTED_IDENTIFIER\",E.IDENTIFIER=\"IDENTIFIER\",E.STRING=\"STRING\",E.VARIABLE=\"VARIABLE\",E.RESERVED_DATA_TYPE=\"RESERVED_DATA_TYPE\",E.RESERVED_PARAMETERIZED_DATA_TYPE=\"RESERVED_PARAMETERIZED_DATA_TYPE\",E.RESERVED_KEYWORD=\"RESERVED_KEYWORD\",E.RESERVED_FUNCTION_NAME=\"RESERVED_FUNCTION_NAME\",E.RESERVED_PHRASE=\"RESERVED_PHRASE\",E.RESERVED_SET_OPERATION=\"RESERVED_SET_OPERATION\",E.RESERVED_CLAUSE=\"RESERVED_CLAUSE\",E.RESERVED_SELECT=\"RESERVED_SELECT\",E.RESERVED_JOIN=\"RESERVED_JOIN\",E.ARRAY_IDENTIFIER=\"ARRAY_IDENTIFIER\",E.ARRAY_KEYWORD=\"ARRAY_KEYWORD\",E.CASE=\"CASE\",E.END=\"END\",E.WHEN=\"WHEN\",E.ELSE=\"ELSE\",E.THEN=\"THEN\",E.LIMIT=\"LIMIT\",E.BETWEEN=\"BETWEEN\",E.AND=\"AND\",E.OR=\"OR\",E.XOR=\"XOR\",E.OPERATOR=\"OPERATOR\",E.COMMA=\"COMMA\",E.ASTERISK=\"ASTERISK\",E.PROPERTY_ACCESS_OPERATOR=\"PROPERTY_ACCESS_OPERATOR\",E.OPEN_PAREN=\"OPEN_PAREN\",E.CLOSE_PAREN=\"CLOSE_PAREN\",E.LINE_COMMENT=\"LINE_COMMENT\",E.BLOCK_COMMENT=\"BLOCK_COMMENT\",E.DISABLE_COMMENT=\"DISABLE_COMMENT\",E.NUMBER=\"NUMBER\",E.NAMED_PARAMETER=\"NAMED_PARAMETER\",E.QUOTED_PARAMETER=\"QUOTED_PARAMETER\",E.NUMBERED_PARAMETER=\"NUMBERED_PARAMETER\",E.POSITIONAL_PARAMETER=\"POSITIONAL_PARAMETER\",E.CUSTOM_PARAMETER=\"CUSTOM_PARAMETER\",E.DELIMITER=\"DELIMITER\",E.EOF=\"EOF\",E))(VR||{}),WR=E=>({type:\"EOF\",raw:\"«EOF»\",text:\"«EOF»\",start:E}),tt=WR(1/0),QE=E=>e=>e.type===E.type&&e.text===E.text,FE={ARRAY:QE({text:\"ARRAY\",type:\"RESERVED_DATA_TYPE\"}),BY:QE({text:\"BY\",type:\"RESERVED_KEYWORD\"}),SET:QE({text:\"SET\",type:\"RESERVED_CLAUSE\"}),STRUCT:QE({text:\"STRUCT\",type:\"RESERVED_DATA_TYPE\"}),WINDOW:QE({text:\"WINDOW\",type:\"RESERVED_CLAUSE\"}),VALUES:QE({text:\"VALUES\",type:\"RESERVED_CLAUSE\"})},wR=E=>E===\"RESERVED_DATA_TYPE\"||E===\"RESERVED_KEYWORD\"||E===\"RESERVED_FUNCTION_NAME\"||E===\"RESERVED_PHRASE\"||E===\"RESERVED_CLAUSE\"||E===\"RESERVED_SELECT\"||E===\"RESERVED_SET_OPERATION\"||E===\"RESERVED_JOIN\"||E===\"ARRAY_KEYWORD\"||E===\"CASE\"||E===\"END\"||E===\"WHEN\"||E===\"ELSE\"||E===\"THEN\"||E===\"LIMIT\"||E===\"BETWEEN\"||E===\"AND\"||E===\"OR\"||E===\"XOR\",is=E=>E===\"AND\"||E===\"OR\"||E===\"XOR\",as=[\"KEYS.NEW_KEYSET\",\"KEYS.ADD_KEY_FROM_RAW_BYTES\",\"AEAD.DECRYPT_BYTES\",\"AEAD.DECRYPT_STRING\",\"AEAD.ENCRYPT\",\"KEYS.KEYSET_CHAIN\",\"KEYS.KEYSET_FROM_JSON\",\"KEYS.KEYSET_TO_JSON\",\"KEYS.ROTATE_KEYSET\",\"KEYS.KEYSET_LENGTH\",\"ANY_VALUE\",\"ARRAY_AGG\",\"AVG\",\"CORR\",\"COUNT\",\"COUNTIF\",\"COVAR_POP\",\"COVAR_SAMP\",\"MAX\",\"MIN\",\"ST_CLUSTERDBSCAN\",\"STDDEV_POP\",\"STDDEV_SAMP\",\"STRING_AGG\",\"SUM\",\"VAR_POP\",\"VAR_SAMP\",\"ANY_VALUE\",\"ARRAY_AGG\",\"ARRAY_CONCAT_AGG\",\"AVG\",\"BIT_AND\",\"BIT_OR\",\"BIT_XOR\",\"COUNT\",\"COUNTIF\",\"LOGICAL_AND\",\"LOGICAL_OR\",\"MAX\",\"MIN\",\"STRING_AGG\",\"SUM\",\"APPROX_COUNT_DISTINCT\",\"APPROX_QUANTILES\",\"APPROX_TOP_COUNT\",\"APPROX_TOP_SUM\",\"ARRAY_CONCAT\",\"ARRAY_LENGTH\",\"ARRAY_TO_STRING\",\"GENERATE_ARRAY\",\"GENERATE_DATE_ARRAY\",\"GENERATE_TIMESTAMP_ARRAY\",\"ARRAY_REVERSE\",\"OFFSET\",\"SAFE_OFFSET\",\"ORDINAL\",\"SAFE_ORDINAL\",\"BIT_COUNT\",\"PARSE_BIGNUMERIC\",\"PARSE_NUMERIC\",\"SAFE_CAST\",\"CURRENT_DATE\",\"EXTRACT\",\"DATE\",\"DATE_ADD\",\"DATE_SUB\",\"DATE_DIFF\",\"DATE_TRUNC\",\"DATE_FROM_UNIX_DATE\",\"FORMAT_DATE\",\"LAST_DAY\",\"PARSE_DATE\",\"UNIX_DATE\",\"CURRENT_DATETIME\",\"DATETIME\",\"EXTRACT\",\"DATETIME_ADD\",\"DATETIME_SUB\",\"DATETIME_DIFF\",\"DATETIME_TRUNC\",\"FORMAT_DATETIME\",\"LAST_DAY\",\"PARSE_DATETIME\",\"ERROR\",\"EXTERNAL_QUERY\",\"S2_CELLIDFROMPOINT\",\"S2_COVERINGCELLIDS\",\"ST_ANGLE\",\"ST_AREA\",\"ST_ASBINARY\",\"ST_ASGEOJSON\",\"ST_ASTEXT\",\"ST_AZIMUTH\",\"ST_BOUNDARY\",\"ST_BOUNDINGBOX\",\"ST_BUFFER\",\"ST_BUFFERWITHTOLERANCE\",\"ST_CENTROID\",\"ST_CENTROID_AGG\",\"ST_CLOSESTPOINT\",\"ST_CLUSTERDBSCAN\",\"ST_CONTAINS\",\"ST_CONVEXHULL\",\"ST_COVEREDBY\",\"ST_COVERS\",\"ST_DIFFERENCE\",\"ST_DIMENSION\",\"ST_DISJOINT\",\"ST_DISTANCE\",\"ST_DUMP\",\"ST_DWITHIN\",\"ST_ENDPOINT\",\"ST_EQUALS\",\"ST_EXTENT\",\"ST_EXTERIORRING\",\"ST_GEOGFROM\",\"ST_GEOGFROMGEOJSON\",\"ST_GEOGFROMTEXT\",\"ST_GEOGFROMWKB\",\"ST_GEOGPOINT\",\"ST_GEOGPOINTFROMGEOHASH\",\"ST_GEOHASH\",\"ST_GEOMETRYTYPE\",\"ST_INTERIORRINGS\",\"ST_INTERSECTION\",\"ST_INTERSECTS\",\"ST_INTERSECTSBOX\",\"ST_ISCOLLECTION\",\"ST_ISEMPTY\",\"ST_LENGTH\",\"ST_MAKELINE\",\"ST_MAKEPOLYGON\",\"ST_MAKEPOLYGONORIENTED\",\"ST_MAXDISTANCE\",\"ST_NPOINTS\",\"ST_NUMGEOMETRIES\",\"ST_NUMPOINTS\",\"ST_PERIMETER\",\"ST_POINTN\",\"ST_SIMPLIFY\",\"ST_SNAPTOGRID\",\"ST_STARTPOINT\",\"ST_TOUCHES\",\"ST_UNION\",\"ST_UNION_AGG\",\"ST_WITHIN\",\"ST_X\",\"ST_Y\",\"FARM_FINGERPRINT\",\"MD5\",\"SHA1\",\"SHA256\",\"SHA512\",\"HLL_COUNT.INIT\",\"HLL_COUNT.MERGE\",\"HLL_COUNT.MERGE_PARTIAL\",\"HLL_COUNT.EXTRACT\",\"MAKE_INTERVAL\",\"EXTRACT\",\"JUSTIFY_DAYS\",\"JUSTIFY_HOURS\",\"JUSTIFY_INTERVAL\",\"JSON_EXTRACT\",\"JSON_QUERY\",\"JSON_EXTRACT_SCALAR\",\"JSON_VALUE\",\"JSON_EXTRACT_ARRAY\",\"JSON_QUERY_ARRAY\",\"JSON_EXTRACT_STRING_ARRAY\",\"JSON_VALUE_ARRAY\",\"TO_JSON_STRING\",\"ABS\",\"SIGN\",\"IS_INF\",\"IS_NAN\",\"IEEE_DIVIDE\",\"RAND\",\"SQRT\",\"POW\",\"POWER\",\"EXP\",\"LN\",\"LOG\",\"LOG10\",\"GREATEST\",\"LEAST\",\"DIV\",\"SAFE_DIVIDE\",\"SAFE_MULTIPLY\",\"SAFE_NEGATE\",\"SAFE_ADD\",\"SAFE_SUBTRACT\",\"MOD\",\"ROUND\",\"TRUNC\",\"CEIL\",\"CEILING\",\"FLOOR\",\"COS\",\"COSH\",\"ACOS\",\"ACOSH\",\"SIN\",\"SINH\",\"ASIN\",\"ASINH\",\"TAN\",\"TANH\",\"ATAN\",\"ATANH\",\"ATAN2\",\"RANGE_BUCKET\",\"FIRST_VALUE\",\"LAST_VALUE\",\"NTH_VALUE\",\"LEAD\",\"LAG\",\"PERCENTILE_CONT\",\"PERCENTILE_DISC\",\"NET.IP_FROM_STRING\",\"NET.SAFE_IP_FROM_STRING\",\"NET.IP_TO_STRING\",\"NET.IP_NET_MASK\",\"NET.IP_TRUNC\",\"NET.IPV4_FROM_INT64\",\"NET.IPV4_TO_INT64\",\"NET.HOST\",\"NET.PUBLIC_SUFFIX\",\"NET.REG_DOMAIN\",\"RANK\",\"DENSE_RANK\",\"PERCENT_RANK\",\"CUME_DIST\",\"NTILE\",\"ROW_NUMBER\",\"SESSION_USER\",\"CORR\",\"COVAR_POP\",\"COVAR_SAMP\",\"STDDEV_POP\",\"STDDEV_SAMP\",\"STDDEV\",\"VAR_POP\",\"VAR_SAMP\",\"VARIANCE\",\"ASCII\",\"BYTE_LENGTH\",\"CHAR_LENGTH\",\"CHARACTER_LENGTH\",\"CHR\",\"CODE_POINTS_TO_BYTES\",\"CODE_POINTS_TO_STRING\",\"CONCAT\",\"CONTAINS_SUBSTR\",\"ENDS_WITH\",\"FORMAT\",\"FROM_BASE32\",\"FROM_BASE64\",\"FROM_HEX\",\"INITCAP\",\"INSTR\",\"LEFT\",\"LENGTH\",\"LPAD\",\"LOWER\",\"LTRIM\",\"NORMALIZE\",\"NORMALIZE_AND_CASEFOLD\",\"OCTET_LENGTH\",\"REGEXP_CONTAINS\",\"REGEXP_EXTRACT\",\"REGEXP_EXTRACT_ALL\",\"REGEXP_INSTR\",\"REGEXP_REPLACE\",\"REGEXP_SUBSTR\",\"REPLACE\",\"REPEAT\",\"REVERSE\",\"RIGHT\",\"RPAD\",\"RTRIM\",\"SAFE_CONVERT_BYTES_TO_STRING\",\"SOUNDEX\",\"SPLIT\",\"STARTS_WITH\",\"STRPOS\",\"SUBSTR\",\"SUBSTRING\",\"TO_BASE32\",\"TO_BASE64\",\"TO_CODE_POINTS\",\"TO_HEX\",\"TRANSLATE\",\"TRIM\",\"UNICODE\",\"UPPER\",\"CURRENT_TIME\",\"TIME\",\"EXTRACT\",\"TIME_ADD\",\"TIME_SUB\",\"TIME_DIFF\",\"TIME_TRUNC\",\"FORMAT_TIME\",\"PARSE_TIME\",\"CURRENT_TIMESTAMP\",\"EXTRACT\",\"STRING\",\"TIMESTAMP\",\"TIMESTAMP_ADD\",\"TIMESTAMP_SUB\",\"TIMESTAMP_DIFF\",\"TIMESTAMP_TRUNC\",\"FORMAT_TIMESTAMP\",\"PARSE_TIMESTAMP\",\"TIMESTAMP_SECONDS\",\"TIMESTAMP_MILLIS\",\"TIMESTAMP_MICROS\",\"UNIX_SECONDS\",\"UNIX_MILLIS\",\"UNIX_MICROS\",\"GENERATE_UUID\",\"COALESCE\",\"IF\",\"IFNULL\",\"NULLIF\",\"AVG\",\"BIT_AND\",\"BIT_OR\",\"BIT_XOR\",\"CORR\",\"COUNT\",\"COVAR_POP\",\"COVAR_SAMP\",\"EXACT_COUNT_DISTINCT\",\"FIRST\",\"GROUP_CONCAT\",\"GROUP_CONCAT_UNQUOTED\",\"LAST\",\"MAX\",\"MIN\",\"NEST\",\"NTH\",\"QUANTILES\",\"STDDEV\",\"STDDEV_POP\",\"STDDEV_SAMP\",\"SUM\",\"TOP\",\"UNIQUE\",\"VARIANCE\",\"VAR_POP\",\"VAR_SAMP\",\"BIT_COUNT\",\"BOOLEAN\",\"BYTES\",\"CAST\",\"FLOAT\",\"HEX_STRING\",\"INTEGER\",\"STRING\",\"COALESCE\",\"GREATEST\",\"IFNULL\",\"IS_INF\",\"IS_NAN\",\"IS_EXPLICITLY_DEFINED\",\"LEAST\",\"NVL\",\"CURRENT_DATE\",\"CURRENT_TIME\",\"CURRENT_TIMESTAMP\",\"DATE\",\"DATE_ADD\",\"DATEDIFF\",\"DAY\",\"DAYOFWEEK\",\"DAYOFYEAR\",\"FORMAT_UTC_USEC\",\"HOUR\",\"MINUTE\",\"MONTH\",\"MSEC_TO_TIMESTAMP\",\"NOW\",\"PARSE_UTC_USEC\",\"QUARTER\",\"SEC_TO_TIMESTAMP\",\"SECOND\",\"STRFTIME_UTC_USEC\",\"TIME\",\"TIMESTAMP\",\"TIMESTAMP_TO_MSEC\",\"TIMESTAMP_TO_SEC\",\"TIMESTAMP_TO_USEC\",\"USEC_TO_TIMESTAMP\",\"UTC_USEC_TO_DAY\",\"UTC_USEC_TO_HOUR\",\"UTC_USEC_TO_MONTH\",\"UTC_USEC_TO_WEEK\",\"UTC_USEC_TO_YEAR\",\"WEEK\",\"YEAR\",\"FORMAT_IP\",\"PARSE_IP\",\"FORMAT_PACKED_IP\",\"PARSE_PACKED_IP\",\"JSON_EXTRACT\",\"JSON_EXTRACT_SCALAR\",\"ABS\",\"ACOS\",\"ACOSH\",\"ASIN\",\"ASINH\",\"ATAN\",\"ATANH\",\"ATAN2\",\"CEIL\",\"COS\",\"COSH\",\"DEGREES\",\"EXP\",\"FLOOR\",\"LN\",\"LOG\",\"LOG2\",\"LOG10\",\"PI\",\"POW\",\"RADIANS\",\"RAND\",\"ROUND\",\"SIN\",\"SINH\",\"SQRT\",\"TAN\",\"TANH\",\"REGEXP_MATCH\",\"REGEXP_EXTRACT\",\"REGEXP_REPLACE\",\"CONCAT\",\"INSTR\",\"LEFT\",\"LENGTH\",\"LOWER\",\"LPAD\",\"LTRIM\",\"REPLACE\",\"RIGHT\",\"RPAD\",\"RTRIM\",\"SPLIT\",\"SUBSTR\",\"UPPER\",\"TABLE_DATE_RANGE\",\"TABLE_DATE_RANGE_STRICT\",\"TABLE_QUERY\",\"HOST\",\"DOMAIN\",\"TLD\",\"AVG\",\"COUNT\",\"MAX\",\"MIN\",\"STDDEV\",\"SUM\",\"CUME_DIST\",\"DENSE_RANK\",\"FIRST_VALUE\",\"LAG\",\"LAST_VALUE\",\"LEAD\",\"NTH_VALUE\",\"NTILE\",\"PERCENT_RANK\",\"PERCENTILE_CONT\",\"PERCENTILE_DISC\",\"RANK\",\"RATIO_TO_REPORT\",\"ROW_NUMBER\",\"CURRENT_USER\",\"EVERY\",\"FROM_BASE64\",\"HASH\",\"FARM_FINGERPRINT\",\"IF\",\"POSITION\",\"SHA1\",\"SOME\",\"TO_BASE64\",\"BQ.JOBS.CANCEL\",\"BQ.REFRESH_MATERIALIZED_VIEW\",\"OPTIONS\",\"PIVOT\",\"UNPIVOT\"],Is=[\"ALL\",\"AND\",\"ANY\",\"AS\",\"ASC\",\"ASSERT_ROWS_MODIFIED\",\"AT\",\"BETWEEN\",\"BY\",\"CASE\",\"CAST\",\"COLLATE\",\"CONTAINS\",\"CREATE\",\"CROSS\",\"CUBE\",\"CURRENT\",\"DEFAULT\",\"DEFINE\",\"DESC\",\"DISTINCT\",\"ELSE\",\"END\",\"ENUM\",\"ESCAPE\",\"EXCEPT\",\"EXCLUDE\",\"EXISTS\",\"EXTRACT\",\"FALSE\",\"FETCH\",\"FOLLOWING\",\"FOR\",\"FROM\",\"FULL\",\"GROUP\",\"GROUPING\",\"GROUPS\",\"HASH\",\"HAVING\",\"IF\",\"IGNORE\",\"IN\",\"INNER\",\"INTERSECT\",\"INTO\",\"IS\",\"JOIN\",\"LATERAL\",\"LEFT\",\"LIMIT\",\"LOOKUP\",\"MERGE\",\"NATURAL\",\"NEW\",\"NO\",\"NOT\",\"NULL\",\"NULLS\",\"OF\",\"ON\",\"OR\",\"ORDER\",\"OUTER\",\"OVER\",\"PARTITION\",\"PRECEDING\",\"PROTO\",\"RANGE\",\"RECURSIVE\",\"RESPECT\",\"RIGHT\",\"ROLLUP\",\"ROWS\",\"SELECT\",\"SET\",\"SOME\",\"TABLE\",\"TABLESAMPLE\",\"THEN\",\"TO\",\"TREAT\",\"TRUE\",\"UNBOUNDED\",\"UNION\",\"UNNEST\",\"USING\",\"WHEN\",\"WHERE\",\"WINDOW\",\"WITH\",\"WITHIN\",\"SAFE\",\"LIKE\",\"COPY\",\"CLONE\",\"IN\",\"OUT\",\"INOUT\",\"RETURNS\",\"LANGUAGE\",\"CASCADE\",\"RESTRICT\",\"DETERMINISTIC\"],Ns=[\"ARRAY\",\"BOOL\",\"BYTES\",\"DATE\",\"DATETIME\",\"GEOGRAPHY\",\"INTERVAL\",\"INT64\",\"INT\",\"SMALLINT\",\"INTEGER\",\"BIGINT\",\"TINYINT\",\"BYTEINT\",\"NUMERIC\",\"DECIMAL\",\"BIGNUMERIC\",\"BIGDECIMAL\",\"FLOAT64\",\"STRING\",\"STRUCT\",\"TIME\",\"TIMEZONE\"],ls=v([\"SELECT [ALL | DISTINCT] [AS STRUCT | AS VALUE]\"]),_s=v([\"WITH [RECURSIVE]\",\"FROM\",\"WHERE\",\"GROUP BY\",\"HAVING\",\"QUALIFY\",\"WINDOW\",\"PARTITION BY\",\"ORDER BY\",\"LIMIT\",\"OFFSET\",\"OMIT RECORD IF\",\"INSERT [INTO]\",\"VALUES\",\"SET\",\"MERGE [INTO]\",\"WHEN [NOT] MATCHED [BY SOURCE | BY TARGET] [THEN]\",\"UPDATE SET\",\"CLUSTER BY\",\"FOR SYSTEM_TIME AS OF\",\"WITH CONNECTION\",\"WITH PARTITION COLUMNS\",\"REMOTE WITH CONNECTION\"]),tr=v([\"CREATE [OR REPLACE] [TEMP|TEMPORARY|SNAPSHOT|EXTERNAL] TABLE [IF NOT EXISTS]\"]),xt=v([\"CREATE [OR REPLACE] [MATERIALIZED] VIEW [IF NOT EXISTS]\",\"UPDATE\",\"DELETE [FROM]\",\"DROP [SNAPSHOT | EXTERNAL] TABLE [IF EXISTS]\",\"ALTER TABLE [IF EXISTS]\",\"ADD COLUMN [IF NOT EXISTS]\",\"DROP COLUMN [IF EXISTS]\",\"RENAME TO\",\"ALTER COLUMN [IF EXISTS]\",\"SET DEFAULT COLLATE\",\"SET OPTIONS\",\"DROP NOT NULL\",\"SET DATA TYPE\",\"ALTER SCHEMA [IF EXISTS]\",\"ALTER [MATERIALIZED] VIEW [IF EXISTS]\",\"ALTER BI_CAPACITY\",\"TRUNCATE TABLE\",\"CREATE SCHEMA [IF NOT EXISTS]\",\"DEFAULT COLLATE\",\"CREATE [OR REPLACE] [TEMP|TEMPORARY|TABLE] FUNCTION [IF NOT EXISTS]\",\"CREATE [OR REPLACE] PROCEDURE [IF NOT EXISTS]\",\"CREATE [OR REPLACE] ROW ACCESS POLICY [IF NOT EXISTS]\",\"GRANT TO\",\"FILTER USING\",\"CREATE CAPACITY\",\"AS JSON\",\"CREATE RESERVATION\",\"CREATE ASSIGNMENT\",\"CREATE SEARCH INDEX [IF NOT EXISTS]\",\"DROP SCHEMA [IF EXISTS]\",\"DROP [MATERIALIZED] VIEW [IF EXISTS]\",\"DROP [TABLE] FUNCTION [IF EXISTS]\",\"DROP PROCEDURE [IF EXISTS]\",\"DROP ROW ACCESS POLICY\",\"DROP ALL ROW ACCESS POLICIES\",\"DROP CAPACITY [IF EXISTS]\",\"DROP RESERVATION [IF EXISTS]\",\"DROP ASSIGNMENT [IF EXISTS]\",\"DROP SEARCH INDEX [IF EXISTS]\",\"DROP [IF EXISTS]\",\"GRANT\",\"REVOKE\",\"DECLARE\",\"EXECUTE IMMEDIATE\",\"LOOP\",\"END LOOP\",\"REPEAT\",\"END REPEAT\",\"WHILE\",\"END WHILE\",\"BREAK\",\"LEAVE\",\"CONTINUE\",\"ITERATE\",\"FOR\",\"END FOR\",\"BEGIN\",\"BEGIN TRANSACTION\",\"COMMIT TRANSACTION\",\"ROLLBACK TRANSACTION\",\"RAISE\",\"RETURN\",\"CALL\",\"ASSERT\",\"EXPORT DATA\"]),Ls=v([\"UNION {ALL | DISTINCT}\",\"EXCEPT DISTINCT\",\"INTERSECT DISTINCT\"]),Cs=v([\"JOIN\",\"{LEFT | RIGHT | FULL} [OUTER] JOIN\",\"{INNER | CROSS} JOIN\"]),us=v([\"TABLESAMPLE SYSTEM\",\"ANY TYPE\",\"ALL COLUMNS\",\"NOT DETERMINISTIC\",\"{ROWS | RANGE} BETWEEN\",\"IS [NOT] DISTINCT FROM\"]),cs={name:\"bigquery\",tokenizerOptions:{reservedSelect:ls,reservedClauses:[..._s,...xt,...tr],reservedSetOperations:Ls,reservedJoins:Cs,reservedPhrases:us,reservedKeywords:Is,reservedDataTypes:Ns,reservedFunctionNames:as,extraParens:[\"[]\"],stringTypes:[{quote:'\"\"\"..\"\"\"',prefixes:[\"R\",\"B\",\"RB\",\"BR\"]},{quote:\"\\'\\'\\'..\\'\\'\\'\",prefixes:[\"R\",\"B\",\"RB\",\"BR\"]},'\"\"-bs',\"''-bs\",{quote:'\"\"-raw',prefixes:[\"R\",\"B\",\"RB\",\"BR\"],requirePrefix:!0},{quote:\"''-raw\",prefixes:[\"R\",\"B\",\"RB\",\"BR\"],requirePrefix:!0}],identTypes:[\"``\"],identChars:{dashes:!0},paramTypes:{positional:!0,named:[\"@\"],quoted:[\"@\"]},variableTypes:[{regex:String.raw`@@\\\\w+`}],lineCommentTypes:[\"--\",\"#\"],operators:[\"&\",\"|\",\"^\",\"~\",\">>\",\"<<\",\"||\",\"=>\"],postProcess:fs},formatOptions:{onelineClauses:[...tr,...xt],tabularOnelineClauses:xt}};function fs(E){return Ps(Ds(E))}function Ps(E){let e=tt;return E.map(T=>T.text===\"OFFSET\"&&e.text===\"[\"?(e=T,AE(EE({},T),{type:\"RESERVED_FUNCTION_NAME\"})):(e=T,T))}function Ds(E){var e;const T=[];for(let t=0;t<E.length;t++){const r=E[t];if((FE.ARRAY(r)||FE.STRUCT(r))&&((e=E[t+1])==null?void 0:e.text)===\"<\"){const R=ds(E,t+1),n=E.slice(t,R+1);T.push({type:\"IDENTIFIER\",raw:n.map(Tr(\"raw\")).join(\"\"),text:n.map(Tr(\"text\")).join(\"\"),start:r.start}),t=R}else T.push(r)}return T}var Tr=E=>e=>e.type===\"IDENTIFIER\"||e.type===\"COMMA\"?e[E]+\" \":e[E];function ds(E,e){let T=0;for(let t=e;t<E.length;t++){const r=E[t];if(r.text===\"<\"?T++:r.text===\">\"?T--:r.text===\">>\"&&(T-=2),T===0)return t}return E.length-1}var ps=[\"ARRAY_AGG\",\"AVG\",\"CORRELATION\",\"COUNT\",\"COUNT_BIG\",\"COVARIANCE\",\"COVARIANCE_SAMP\",\"CUME_DIST\",\"GROUPING\",\"LISTAGG\",\"MAX\",\"MEDIAN\",\"MIN\",\"PERCENTILE_CONT\",\"PERCENTILE_DISC\",\"PERCENT_RANK\",\"REGR_AVGX\",\"REGR_AVGY\",\"REGR_COUNT\",\"REGR_INTERCEPT\",\"REGR_ICPT\",\"REGR_R2\",\"REGR_SLOPE\",\"REGR_SXX\",\"REGR_SXY\",\"REGR_SYY\",\"STDDEV\",\"STDDEV_SAMP\",\"SUM\",\"VARIANCE\",\"VARIANCE_SAMP\",\"XMLAGG\",\"XMLGROUP\",\"ABS\",\"ABSVAL\",\"ACOS\",\"ADD_DAYS\",\"ADD_HOURS\",\"ADD_MINUTES\",\"ADD_MONTHS\",\"ADD_SECONDS\",\"ADD_YEARS\",\"AGE\",\"ARRAY_DELETE\",\"ARRAY_FIRST\",\"ARRAY_LAST\",\"ARRAY_NEXT\",\"ARRAY_PRIOR\",\"ASCII\",\"ASCII_STR\",\"ASIN\",\"ATAN\",\"ATAN2\",\"ATANH\",\"BITAND\",\"BITANDNOT\",\"BITOR\",\"BITXOR\",\"BITNOT\",\"BPCHAR\",\"BSON_TO_JSON\",\"BTRIM\",\"CARDINALITY\",\"CEILING\",\"CEIL\",\"CHARACTER_LENGTH\",\"CHR\",\"COALESCE\",\"COLLATION_KEY\",\"COLLATION_KEY_BIT\",\"COMPARE_DECFLOAT\",\"CONCAT\",\"COS\",\"COSH\",\"COT\",\"CURSOR_ROWCOUNT\",\"DATAPARTITIONNUM\",\"DATE_PART\",\"DATE_TRUNC\",\"DAY\",\"DAYNAME\",\"DAYOFMONTH\",\"DAYOFWEEK\",\"DAYOFWEEK_ISO\",\"DAYOFYEAR\",\"DAYS\",\"DAYS_BETWEEN\",\"DAYS_TO_END_OF_MONTH\",\"DBPARTITIONNUM\",\"DECFLOAT\",\"DECFLOAT_FORMAT\",\"DECODE\",\"DECRYPT_BIN\",\"DECRYPT_CHAR\",\"DEGREES\",\"DEREF\",\"DIFFERENCE\",\"DIGITS\",\"DOUBLE_PRECISION\",\"EMPTY_BLOB\",\"EMPTY_CLOB\",\"EMPTY_DBCLOB\",\"EMPTY_NCLOB\",\"ENCRYPT\",\"EVENT_MON_STATE\",\"EXP\",\"EXTRACT\",\"FIRST_DAY\",\"FLOOR\",\"FROM_UTC_TIMESTAMP\",\"GENERATE_UNIQUE\",\"GETHINT\",\"GREATEST\",\"HASH\",\"HASH4\",\"HASH8\",\"HASHEDVALUE\",\"HEX\",\"HEXTORAW\",\"HOUR\",\"HOURS_BETWEEN\",\"IDENTITY_VAL_LOCAL\",\"IFNULL\",\"INITCAP\",\"INSERT\",\"INSTR\",\"INSTR2\",\"INSTR4\",\"INSTRB\",\"INTNAND\",\"INTNOR\",\"INTNXOR\",\"INTNNOT\",\"ISNULL\",\"JSON_ARRAY\",\"JSON_OBJECT\",\"JSON_QUERY\",\"JSON_TO_BSON\",\"JSON_VALUE\",\"JULIAN_DAY\",\"LAST_DAY\",\"LCASE\",\"LEAST\",\"LEFT\",\"LENGTH\",\"LENGTH2\",\"LENGTH4\",\"LENGTHB\",\"LN\",\"LOCATE\",\"LOCATE_IN_STRING\",\"LOG10\",\"LONG_VARCHAR\",\"LONG_VARGRAPHIC\",\"LOWER\",\"LPAD\",\"LTRIM\",\"MAX\",\"MAX_CARDINALITY\",\"MICROSECOND\",\"MIDNIGHT_SECONDS\",\"MIN\",\"MINUTE\",\"MINUTES_BETWEEN\",\"MOD\",\"MONTH\",\"MONTHNAME\",\"MONTHS_BETWEEN\",\"MULTIPLY_ALT\",\"NEXT_DAY\",\"NEXT_MONTH\",\"NEXT_QUARTER\",\"NEXT_WEEK\",\"NEXT_YEAR\",\"NORMALIZE_DECFLOAT\",\"NOW\",\"NULLIF\",\"NVL\",\"NVL2\",\"OCTET_LENGTH\",\"OVERLAY\",\"PARAMETER\",\"POSITION\",\"POSSTR\",\"POW\",\"POWER\",\"QUANTIZE\",\"QUARTER\",\"QUOTE_IDENT\",\"QUOTE_LITERAL\",\"RADIANS\",\"RAISE_ERROR\",\"RAND\",\"RANDOM\",\"RAWTOHEX\",\"REC2XML\",\"REGEXP_COUNT\",\"REGEXP_EXTRACT\",\"REGEXP_INSTR\",\"REGEXP_LIKE\",\"REGEXP_MATCH_COUNT\",\"REGEXP_REPLACE\",\"REGEXP_SUBSTR\",\"REPEAT\",\"REPLACE\",\"RID\",\"RID_BIT\",\"RIGHT\",\"ROUND\",\"ROUND_TIMESTAMP\",\"RPAD\",\"RTRIM\",\"SECLABEL\",\"SECLABEL_BY_NAME\",\"SECLABEL_TO_CHAR\",\"SECOND\",\"SECONDS_BETWEEN\",\"SIGN\",\"SIN\",\"SINH\",\"SOUNDEX\",\"SPACE\",\"SQRT\",\"STRIP\",\"STRLEFT\",\"STRPOS\",\"STRRIGHT\",\"SUBSTR\",\"SUBSTR2\",\"SUBSTR4\",\"SUBSTRB\",\"SUBSTRING\",\"TABLE_NAME\",\"TABLE_SCHEMA\",\"TAN\",\"TANH\",\"THIS_MONTH\",\"THIS_QUARTER\",\"THIS_WEEK\",\"THIS_YEAR\",\"TIMESTAMP_FORMAT\",\"TIMESTAMP_ISO\",\"TIMESTAMPDIFF\",\"TIMEZONE\",\"TO_CHAR\",\"TO_CLOB\",\"TO_DATE\",\"TO_HEX\",\"TO_MULTI_BYTE\",\"TO_NCHAR\",\"TO_NCLOB\",\"TO_NUMBER\",\"TO_SINGLE_BYTE\",\"TO_TIMESTAMP\",\"TO_UTC_TIMESTAMP\",\"TOTALORDER\",\"TRANSLATE\",\"TRIM\",\"TRIM_ARRAY\",\"TRUNC_TIMESTAMP\",\"TRUNCATE\",\"TRUNC\",\"TYPE_ID\",\"TYPE_NAME\",\"TYPE_SCHEMA\",\"UCASE\",\"UNICODE_STR\",\"UPPER\",\"VALUE\",\"VARCHAR_BIT_FORMAT\",\"VARCHAR_FORMAT\",\"VARCHAR_FORMAT_BIT\",\"VERIFY_GROUP_FOR_USER\",\"VERIFY_ROLE_FOR_USER\",\"VERIFY_TRUSTED_CONTEXT_ROLE_FOR_USER\",\"WEEK\",\"WEEK_ISO\",\"WEEKS_BETWEEN\",\"WIDTH_BUCKET\",\"XMLATTRIBUTES\",\"XMLCOMMENT\",\"XMLCONCAT\",\"XMLDOCUMENT\",\"XMLELEMENT\",\"XMLFOREST\",\"XMLNAMESPACES\",\"XMLPARSE\",\"XMLPI\",\"XMLQUERY\",\"XMLROW\",\"XMLSERIALIZE\",\"XMLTEXT\",\"XMLVALIDATE\",\"XMLXSROBJECTID\",\"XSLTRANSFORM\",\"YEAR\",\"YEARS_BETWEEN\",\"YMD_BETWEEN\",\"BASE_TABLE\",\"JSON_TABLE\",\"UNNEST\",\"XMLTABLE\",\"RANK\",\"DENSE_RANK\",\"NTILE\",\"LAG\",\"LEAD\",\"ROW_NUMBER\",\"FIRST_VALUE\",\"LAST_VALUE\",\"NTH_VALUE\",\"RATIO_TO_REPORT\",\"CAST\"],Ms=[\"ACTIVATE\",\"ADD\",\"AFTER\",\"ALIAS\",\"ALL\",\"ALLOCATE\",\"ALLOW\",\"ALTER\",\"AND\",\"ANY\",\"AS\",\"ASENSITIVE\",\"ASSOCIATE\",\"ASUTIME\",\"AT\",\"ATTRIBUTES\",\"AUDIT\",\"AUTHORIZATION\",\"AUX\",\"AUXILIARY\",\"BEFORE\",\"BEGIN\",\"BETWEEN\",\"BINARY\",\"BUFFERPOOL\",\"BY\",\"CACHE\",\"CALL\",\"CALLED\",\"CAPTURE\",\"CARDINALITY\",\"CASCADED\",\"CASE\",\"CAST\",\"CHECK\",\"CLONE\",\"CLOSE\",\"CLUSTER\",\"COLLECTION\",\"COLLID\",\"COLUMN\",\"COMMENT\",\"COMMIT\",\"CONCAT\",\"CONDITION\",\"CONNECT\",\"CONNECTION\",\"CONSTRAINT\",\"CONTAINS\",\"CONTINUE\",\"COUNT\",\"COUNT_BIG\",\"CREATE\",\"CROSS\",\"CURRENT\",\"CURRENT_DATE\",\"CURRENT_LC_CTYPE\",\"CURRENT_PATH\",\"CURRENT_SCHEMA\",\"CURRENT_SERVER\",\"CURRENT_TIME\",\"CURRENT_TIMESTAMP\",\"CURRENT_TIMEZONE\",\"CURRENT_USER\",\"CURSOR\",\"CYCLE\",\"DATA\",\"DATABASE\",\"DATAPARTITIONNAME\",\"DATAPARTITIONNUM\",\"DAY\",\"DAYS\",\"DB2GENERAL\",\"DB2GENRL\",\"DB2SQL\",\"DBINFO\",\"DBPARTITIONNAME\",\"DBPARTITIONNUM\",\"DEALLOCATE\",\"DECLARE\",\"DEFAULT\",\"DEFAULTS\",\"DEFINITION\",\"DELETE\",\"DENSERANK\",\"DENSE_RANK\",\"DESCRIBE\",\"DESCRIPTOR\",\"DETERMINISTIC\",\"DIAGNOSTICS\",\"DISABLE\",\"DISALLOW\",\"DISCONNECT\",\"DISTINCT\",\"DO\",\"DOCUMENT\",\"DROP\",\"DSSIZE\",\"DYNAMIC\",\"EACH\",\"EDITPROC\",\"ELSE\",\"ELSEIF\",\"ENABLE\",\"ENCODING\",\"ENCRYPTION\",\"END\",\"END-EXEC\",\"ENDING\",\"ERASE\",\"ESCAPE\",\"EVERY\",\"EXCEPT\",\"EXCEPTION\",\"EXCLUDING\",\"EXCLUSIVE\",\"EXECUTE\",\"EXISTS\",\"EXIT\",\"EXPLAIN\",\"EXTENDED\",\"EXTERNAL\",\"EXTRACT\",\"FENCED\",\"FETCH\",\"FIELDPROC\",\"FILE\",\"FINAL\",\"FIRST1\",\"FOR\",\"FOREIGN\",\"FREE\",\"FROM\",\"FULL\",\"FUNCTION\",\"GENERAL\",\"GENERATED\",\"GET\",\"GLOBAL\",\"GO\",\"GOTO\",\"GRANT\",\"GRAPHIC\",\"GROUP\",\"HANDLER\",\"HASH\",\"HASHED_VALUE\",\"HAVING\",\"HINT\",\"HOLD\",\"HOUR\",\"HOURS\",\"IDENTITY\",\"IF\",\"IMMEDIATE\",\"IMPORT\",\"IN\",\"INCLUDING\",\"INCLUSIVE\",\"INCREMENT\",\"INDEX\",\"INDICATOR\",\"INDICATORS\",\"INF\",\"INFINITY\",\"INHERIT\",\"INNER\",\"INOUT\",\"INSENSITIVE\",\"INSERT\",\"INTEGRITY\",\"INTERSECT\",\"INTO\",\"IS\",\"ISNULL\",\"ISOBID\",\"ISOLATION\",\"ITERATE\",\"JAR\",\"JAVA\",\"JOIN\",\"KEEP\",\"KEY\",\"LABEL\",\"LANGUAGE\",\"LAST3\",\"LATERAL\",\"LC_CTYPE\",\"LEAVE\",\"LEFT\",\"LIKE\",\"LIMIT\",\"LINKTYPE\",\"LOCAL\",\"LOCALDATE\",\"LOCALE\",\"LOCALTIME\",\"LOCALTIMESTAMP\",\"LOCATOR\",\"LOCATORS\",\"LOCK\",\"LOCKMAX\",\"LOCKSIZE\",\"LOOP\",\"MAINTAINED\",\"MATERIALIZED\",\"MAXVALUE\",\"MICROSECOND\",\"MICROSECONDS\",\"MINUTE\",\"MINUTES\",\"MINVALUE\",\"MODE\",\"MODIFIES\",\"MONTH\",\"MONTHS\",\"NAN\",\"NEW\",\"NEW_TABLE\",\"NEXTVAL\",\"NO\",\"NOCACHE\",\"NOCYCLE\",\"NODENAME\",\"NODENUMBER\",\"NOMAXVALUE\",\"NOMINVALUE\",\"NONE\",\"NOORDER\",\"NORMALIZED\",\"NOT2\",\"NOTNULL\",\"NULL\",\"NULLS\",\"NUMPARTS\",\"OBID\",\"OF\",\"OFF\",\"OFFSET\",\"OLD\",\"OLD_TABLE\",\"ON\",\"OPEN\",\"OPTIMIZATION\",\"OPTIMIZE\",\"OPTION\",\"OR\",\"ORDER\",\"OUT\",\"OUTER\",\"OVER\",\"OVERRIDING\",\"PACKAGE\",\"PADDED\",\"PAGESIZE\",\"PARAMETER\",\"PART\",\"PARTITION\",\"PARTITIONED\",\"PARTITIONING\",\"PARTITIONS\",\"PASSWORD\",\"PATH\",\"PERCENT\",\"PIECESIZE\",\"PLAN\",\"POSITION\",\"PRECISION\",\"PREPARE\",\"PREVVAL\",\"PRIMARY\",\"PRIQTY\",\"PRIVILEGES\",\"PROCEDURE\",\"PROGRAM\",\"PSID\",\"PUBLIC\",\"QUERY\",\"QUERYNO\",\"RANGE\",\"RANK\",\"READ\",\"READS\",\"RECOVERY\",\"REFERENCES\",\"REFERENCING\",\"REFRESH\",\"RELEASE\",\"RENAME\",\"REPEAT\",\"RESET\",\"RESIGNAL\",\"RESTART\",\"RESTRICT\",\"RESULT\",\"RESULT_SET_LOCATOR\",\"RETURN\",\"RETURNS\",\"REVOKE\",\"RIGHT\",\"ROLE\",\"ROLLBACK\",\"ROUND_CEILING\",\"ROUND_DOWN\",\"ROUND_FLOOR\",\"ROUND_HALF_DOWN\",\"ROUND_HALF_EVEN\",\"ROUND_HALF_UP\",\"ROUND_UP\",\"ROUTINE\",\"ROW\",\"ROWNUMBER\",\"ROWS\",\"ROWSET\",\"ROW_NUMBER\",\"RRN\",\"RUN\",\"SAVEPOINT\",\"SCHEMA\",\"SCRATCHPAD\",\"SCROLL\",\"SEARCH\",\"SECOND\",\"SECONDS\",\"SECQTY\",\"SECURITY\",\"SELECT\",\"SENSITIVE\",\"SEQUENCE\",\"SESSION\",\"SESSION_USER\",\"SET\",\"SIGNAL\",\"SIMPLE\",\"SNAN\",\"SOME\",\"SOURCE\",\"SPECIFIC\",\"SQL\",\"SQLID\",\"STACKED\",\"STANDARD\",\"START\",\"STARTING\",\"STATEMENT\",\"STATIC\",\"STATMENT\",\"STAY\",\"STOGROUP\",\"STORES\",\"STYLE\",\"SUBSTRING\",\"SUMMARY\",\"SYNONYM\",\"SYSFUN\",\"SYSIBM\",\"SYSPROC\",\"SYSTEM\",\"SYSTEM_USER\",\"TABLE\",\"TABLESPACE\",\"THEN\",\"TO\",\"TRANSACTION\",\"TRIGGER\",\"TRIM\",\"TRUNCATE\",\"TYPE\",\"UNDO\",\"UNION\",\"UNIQUE\",\"UNTIL\",\"UPDATE\",\"USAGE\",\"USER\",\"USING\",\"VALIDPROC\",\"VALUE\",\"VALUES\",\"VARIABLE\",\"VARIANT\",\"VCAT\",\"VERSION\",\"VIEW\",\"VOLATILE\",\"VOLUMES\",\"WHEN\",\"WHENEVER\",\"WHERE\",\"WHILE\",\"WITH\",\"WITHOUT\",\"WLM\",\"WRITE\",\"XMLELEMENT\",\"XMLEXISTS\",\"XMLNAMESPACES\",\"YEAR\",\"YEARS\"],Us=[\"ARRAY\",\"BIGINT\",\"BINARY\",\"BLOB\",\"BOOLEAN\",\"CCSID\",\"CHAR\",\"CHARACTER\",\"CLOB\",\"DATE\",\"DATETIME\",\"DBCLOB\",\"DEC\",\"DECIMAL\",\"DOUBLE\",\"DOUBLE PRECISION\",\"FLOAT\",\"FLOAT4\",\"FLOAT8\",\"GRAPHIC\",\"INT\",\"INT2\",\"INT4\",\"INT8\",\"INTEGER\",\"INTERVAL\",\"LONG VARCHAR\",\"LONG VARGRAPHIC\",\"NCHAR\",\"NCHR\",\"NCLOB\",\"NVARCHAR\",\"NUMERIC\",\"SMALLINT\",\"REAL\",\"TIME\",\"TIMESTAMP\",\"VARBINARY\",\"VARCHAR\",\"VARGRAPHIC\"],ms=v([\"SELECT [ALL | DISTINCT]\"]),hs=v([\"WITH\",\"FROM\",\"WHERE\",\"GROUP BY\",\"HAVING\",\"PARTITION BY\",\"ORDER BY [INPUT SEQUENCE]\",\"LIMIT\",\"OFFSET\",\"FETCH NEXT\",\"FOR UPDATE [OF]\",\"FOR {READ | FETCH} ONLY\",\"FOR {RR | CS | UR | RS} [USE AND KEEP {SHARE | UPDATE | EXCLUSIVE} LOCKS]\",\"WAIT FOR OUTCOME\",\"SKIP LOCKED DATA\",\"INTO\",\"INSERT INTO\",\"VALUES\",\"SET\",\"MERGE INTO\",\"WHEN [NOT] MATCHED [THEN]\",\"UPDATE SET\",\"INSERT\"]),rr=v([\"CREATE [GLOBAL TEMPORARY | EXTERNAL] TABLE [IF NOT EXISTS]\"]),Xt=v([\"CREATE [OR REPLACE] VIEW\",\"UPDATE\",\"WHERE CURRENT OF\",\"WITH {RR | RS | CS | UR}\",\"DELETE FROM\",\"DROP TABLE [IF EXISTS]\",\"ALTER TABLE\",\"ADD [COLUMN]\",\"DROP [COLUMN]\",\"RENAME COLUMN\",\"ALTER [COLUMN]\",\"SET DATA TYPE\",\"SET NOT NULL\",\"DROP {DEFAULT | GENERATED | NOT NULL}\",\"TRUNCATE [TABLE]\",\"ALLOCATE\",\"ALTER AUDIT POLICY\",\"ALTER BUFFERPOOL\",\"ALTER DATABASE PARTITION GROUP\",\"ALTER DATABASE\",\"ALTER EVENT MONITOR\",\"ALTER FUNCTION\",\"ALTER HISTOGRAM TEMPLATE\",\"ALTER INDEX\",\"ALTER MASK\",\"ALTER METHOD\",\"ALTER MODULE\",\"ALTER NICKNAME\",\"ALTER PACKAGE\",\"ALTER PERMISSION\",\"ALTER PROCEDURE\",\"ALTER SCHEMA\",\"ALTER SECURITY LABEL COMPONENT\",\"ALTER SECURITY POLICY\",\"ALTER SEQUENCE\",\"ALTER SERVER\",\"ALTER SERVICE CLASS\",\"ALTER STOGROUP\",\"ALTER TABLESPACE\",\"ALTER THRESHOLD\",\"ALTER TRIGGER\",\"ALTER TRUSTED CONTEXT\",\"ALTER TYPE\",\"ALTER USAGE LIST\",\"ALTER USER MAPPING\",\"ALTER VIEW\",\"ALTER WORK ACTION SET\",\"ALTER WORK CLASS SET\",\"ALTER WORKLOAD\",\"ALTER WRAPPER\",\"ALTER XSROBJECT\",\"ALTER STOGROUP\",\"ALTER TABLESPACE\",\"ALTER TRIGGER\",\"ALTER TRUSTED CONTEXT\",\"ALTER VIEW\",\"ASSOCIATE [RESULT SET] {LOCATOR | LOCATORS}\",\"AUDIT\",\"BEGIN DECLARE SECTION\",\"CALL\",\"CLOSE\",\"COMMENT ON\",\"COMMIT [WORK]\",\"CONNECT\",\"CREATE [OR REPLACE] [PUBLIC] ALIAS\",\"CREATE AUDIT POLICY\",\"CREATE BUFFERPOOL\",\"CREATE DATABASE PARTITION GROUP\",\"CREATE EVENT MONITOR\",\"CREATE [OR REPLACE] FUNCTION\",\"CREATE FUNCTION MAPPING\",\"CREATE HISTOGRAM TEMPLATE\",\"CREATE [UNIQUE] INDEX\",\"CREATE INDEX EXTENSION\",\"CREATE [OR REPLACE] MASK\",\"CREATE [SPECIFIC] METHOD\",\"CREATE [OR REPLACE] MODULE\",\"CREATE [OR REPLACE] NICKNAME\",\"CREATE [OR REPLACE] PERMISSION\",\"CREATE [OR REPLACE] PROCEDURE\",\"CREATE ROLE\",\"CREATE SCHEMA\",\"CREATE SECURITY LABEL [COMPONENT]\",\"CREATE SECURITY POLICY\",\"CREATE [OR REPLACE] SEQUENCE\",\"CREATE SERVICE CLASS\",\"CREATE SERVER\",\"CREATE STOGROUP\",\"CREATE SYNONYM\",\"CREATE [LARGE | REGULAR | {SYSTEM | USER} TEMPORARY] TABLESPACE\",\"CREATE THRESHOLD\",\"CREATE {TRANSFORM | TRANSFORMS} FOR\",\"CREATE [OR REPLACE] TRIGGER\",\"CREATE TRUSTED CONTEXT\",\"CREATE [OR REPLACE] TYPE\",\"CREATE TYPE MAPPING\",\"CREATE USAGE LIST\",\"CREATE USER MAPPING FOR\",\"CREATE [OR REPLACE] VARIABLE\",\"CREATE WORK ACTION SET\",\"CREATE WORK CLASS SET\",\"CREATE WORKLOAD\",\"CREATE WRAPPER\",\"DECLARE\",\"DECLARE GLOBAL TEMPORARY TABLE\",\"DESCRIBE [INPUT | OUTPUT]\",\"DISCONNECT\",\"DROP [PUBLIC] ALIAS\",\"DROP AUDIT POLICY\",\"DROP BUFFERPOOL\",\"DROP DATABASE PARTITION GROUP\",\"DROP EVENT MONITOR\",\"DROP [SPECIFIC] FUNCTION\",\"DROP FUNCTION MAPPING\",\"DROP HISTOGRAM TEMPLATE\",\"DROP INDEX [EXTENSION]\",\"DROP MASK\",\"DROP [SPECIFIC] METHOD\",\"DROP MODULE\",\"DROP NICKNAME\",\"DROP PACKAGE\",\"DROP PERMISSION\",\"DROP [SPECIFIC] PROCEDURE\",\"DROP ROLE\",\"DROP SCHEMA\",\"DROP SECURITY LABEL [COMPONENT]\",\"DROP SECURITY POLICY\",\"DROP SEQUENCE\",\"DROP SERVER\",\"DROP SERVICE CLASS\",\"DROP STOGROUP\",\"DROP TABLE HIERARCHY\",\"DROP {TABLESPACE | TABLESPACES}\",\"DROP {TRANSFORM | TRANSFORMS}\",\"DROP THRESHOLD\",\"DROP TRIGGER\",\"DROP TRUSTED CONTEXT\",\"DROP TYPE [MAPPING]\",\"DROP USAGE LIST\",\"DROP USER MAPPING FOR\",\"DROP VARIABLE\",\"DROP VIEW [HIERARCHY]\",\"DROP WORK {ACTION | CLASS} SET\",\"DROP WORKLOAD\",\"DROP WRAPPER\",\"DROP XSROBJECT\",\"END DECLARE SECTION\",\"EXECUTE [IMMEDIATE]\",\"EXPLAIN {PLAN [SECTION] | ALL}\",\"FETCH [FROM]\",\"FLUSH {BUFFERPOOL | BUFFERPOOLS} ALL\",\"FLUSH EVENT MONITOR\",\"FLUSH FEDERATED CACHE\",\"FLUSH OPTIMIZATION PROFILE CACHE\",\"FLUSH PACKAGE CACHE [DYNAMIC]\",\"FLUSH AUTHENTICATION CACHE [FOR ALL]\",\"FREE LOCATOR\",\"GET DIAGNOSTICS\",\"GOTO\",\"GRANT\",\"INCLUDE\",\"ITERATE\",\"LEAVE\",\"LOCK TABLE\",\"LOOP\",\"OPEN\",\"PIPE\",\"PREPARE\",\"REFRESH TABLE\",\"RELEASE\",\"RELEASE [TO] SAVEPOINT\",\"RENAME [TABLE | INDEX | STOGROUP | TABLESPACE]\",\"REPEAT\",\"RESIGNAL\",\"RETURN\",\"REVOKE\",\"ROLLBACK [WORK] [TO SAVEPOINT]\",\"SAVEPOINT\",\"SET COMPILATION ENVIRONMENT\",\"SET CONNECTION\",\"SET CURRENT\",\"SET ENCRYPTION PASSWORD\",\"SET EVENT MONITOR STATE\",\"SET INTEGRITY\",\"SET PASSTHRU\",\"SET PATH\",\"SET ROLE\",\"SET SCHEMA\",\"SET SERVER OPTION\",\"SET {SESSION AUTHORIZATION | SESSION_USER}\",\"SET USAGE LIST\",\"SIGNAL\",\"TRANSFER OWNERSHIP OF\",\"WHENEVER {NOT FOUND | SQLERROR | SQLWARNING}\",\"WHILE\"]),Gs=v([\"UNION [ALL]\",\"EXCEPT [ALL]\",\"INTERSECT [ALL]\"]),gs=v([\"JOIN\",\"{LEFT | RIGHT | FULL} [OUTER] JOIN\",\"{INNER | CROSS} JOIN\"]),Hs=v([\"ON DELETE\",\"ON UPDATE\",\"SET NULL\",\"{ROWS | RANGE} BETWEEN\"]),bs={name:\"db2\",tokenizerOptions:{reservedSelect:ms,reservedClauses:[...hs,...rr,...Xt],reservedSetOperations:Gs,reservedJoins:gs,reservedPhrases:Hs,reservedKeywords:Ms,reservedDataTypes:Us,reservedFunctionNames:ps,extraParens:[\"[]\"],stringTypes:[{quote:\"''-qq\",prefixes:[\"G\",\"N\",\"U&\"]},{quote:\"''-raw\",prefixes:[\"X\",\"BX\",\"GX\",\"UX\"],requirePrefix:!0}],identTypes:['\"\"-qq'],identChars:{first:\"@#$\",rest:\"@#$\"},paramTypes:{positional:!0,named:[\":\"]},paramChars:{first:\"@#$\",rest:\"@#$\"},operators:[\"**\",\"%\",\"|\",\"&\",\"^\",\"~\",\"¬=\",\"¬>\",\"¬<\",\"!>\",\"!<\",\"^=\",\"^>\",\"^<\",\"||\",\"->\",\"=>\"]},formatOptions:{onelineClauses:[...rr,...Xt],tabularOnelineClauses:Xt}},ys=[\"ARRAY_AGG\",\"AVG\",\"CORR\",\"CORRELATION\",\"COUNT\",\"COUNT_BIG\",\"COVAR_POP\",\"COVARIANCE\",\"COVAR\",\"COVAR_SAMP\",\"COVARIANCE_SAMP\",\"EVERY\",\"GROUPING\",\"JSON_ARRAYAGG\",\"JSON_OBJECTAGG\",\"LISTAGG\",\"MAX\",\"MEDIAN\",\"MIN\",\"PERCENTILE_CONT\",\"PERCENTILE_DISC\",\"REGR_AVGX\",\"REGR_AVGY\",\"REGR_COUNT\",\"REGR_INTERCEPT\",\"REGR_R2\",\"REGR_SLOPE\",\"REGR_SXX\",\"REGR_SXY\",\"REGR_SYY\",\"SOME\",\"STDDEV_POP\",\"STDDEV\",\"STDDEV_SAMP\",\"SUM\",\"VAR_POP\",\"VARIANCE\",\"VAR\",\"VAR_SAMP\",\"VARIANCE_SAMP\",\"XMLAGG\",\"XMLGROUP\",\"ABS\",\"ABSVAL\",\"ACOS\",\"ADD_DAYS\",\"ADD_HOURS\",\"ADD_MINUTES\",\"ADD_MONTHS\",\"ADD_SECONDS\",\"ADD_YEARS\",\"ANTILOG\",\"ARRAY_MAX_CARDINALITY\",\"ARRAY_TRIM\",\"ASCII\",\"ASIN\",\"ATAN\",\"ATAN2\",\"ATANH\",\"BASE64_DECODE\",\"BASE64_ENCODE\",\"BIT_LENGTH\",\"BITAND\",\"BITANDNOT\",\"BITNOT\",\"BITOR\",\"BITXOR\",\"BSON_TO_JSON\",\"CARDINALITY\",\"CEIL\",\"CEILING\",\"CHAR_LENGTH\",\"CHARACTER_LENGTH\",\"CHR\",\"COALESCE\",\"COMPARE_DECFLOAT\",\"CONCAT\",\"CONTAINS\",\"COS\",\"COSH\",\"COT\",\"CURDATE\",\"CURTIME\",\"DATABASE\",\"DATAPARTITIONNAME\",\"DATAPARTITIONNUM\",\"DAY\",\"DAYNAME\",\"DAYOFMONTH\",\"DAYOFWEEK_ISO\",\"DAYOFWEEK\",\"DAYOFYEAR\",\"DAYS\",\"DBPARTITIONNAME\",\"DBPARTITIONNUM\",\"DECFLOAT_FORMAT\",\"DECFLOAT_SORTKEY\",\"DECRYPT_BINARY\",\"DECRYPT_BIT\",\"DECRYPT_CHAR\",\"DECRYPT_DB\",\"DEGREES\",\"DIFFERENCE\",\"DIGITS\",\"DLCOMMENT\",\"DLLINKTYPE\",\"DLURLCOMPLETE\",\"DLURLPATH\",\"DLURLPATHONLY\",\"DLURLSCHEME\",\"DLURLSERVER\",\"DLVALUE\",\"DOUBLE_PRECISION\",\"DOUBLE\",\"ENCRPYT\",\"ENCRYPT_AES\",\"ENCRYPT_AES256\",\"ENCRYPT_RC2\",\"ENCRYPT_TDES\",\"EXP\",\"EXTRACT\",\"FIRST_DAY\",\"FLOOR\",\"GENERATE_UNIQUE\",\"GET_BLOB_FROM_FILE\",\"GET_CLOB_FROM_FILE\",\"GET_DBCLOB_FROM_FILE\",\"GET_XML_FILE\",\"GETHINT\",\"GREATEST\",\"HASH_MD5\",\"HASH_ROW\",\"HASH_SHA1\",\"HASH_SHA256\",\"HASH_SHA512\",\"HASH_VALUES\",\"HASHED_VALUE\",\"HEX\",\"HEXTORAW\",\"HOUR\",\"HTML_ENTITY_DECODE\",\"HTML_ENTITY_ENCODE\",\"HTTP_DELETE_BLOB\",\"HTTP_DELETE\",\"HTTP_GET_BLOB\",\"HTTP_GET\",\"HTTP_PATCH_BLOB\",\"HTTP_PATCH\",\"HTTP_POST_BLOB\",\"HTTP_POST\",\"HTTP_PUT_BLOB\",\"HTTP_PUT\",\"IDENTITY_VAL_LOCAL\",\"IFNULL\",\"INSERT\",\"INSTR\",\"INTERPRET\",\"ISFALSE\",\"ISNOTFALSE\",\"ISNOTTRUE\",\"ISTRUE\",\"JSON_ARRAY\",\"JSON_OBJECT\",\"JSON_QUERY\",\"JSON_TO_BSON\",\"JSON_UPDATE\",\"JSON_VALUE\",\"JULIAN_DAY\",\"LAND\",\"LAST_DAY\",\"LCASE\",\"LEAST\",\"LEFT\",\"LENGTH\",\"LN\",\"LNOT\",\"LOCATE_IN_STRING\",\"LOCATE\",\"LOG10\",\"LOR\",\"LOWER\",\"LPAD\",\"LTRIM\",\"MAX_CARDINALITY\",\"MAX\",\"MICROSECOND\",\"MIDNIGHT_SECONDS\",\"MIN\",\"MINUTE\",\"MOD\",\"MONTH\",\"MONTHNAME\",\"MONTHS_BETWEEN\",\"MQREAD\",\"MQREADCLOB\",\"MQRECEIVE\",\"MQRECEIVECLOB\",\"MQSEND\",\"MULTIPLY_ALT\",\"NEXT_DAY\",\"NORMALIZE_DECFLOAT\",\"NOW\",\"NULLIF\",\"NVL\",\"OCTET_LENGTH\",\"OVERLAY\",\"PI\",\"POSITION\",\"POSSTR\",\"POW\",\"POWER\",\"QUANTIZE\",\"QUARTER\",\"RADIANS\",\"RAISE_ERROR\",\"RANDOM\",\"RAND\",\"REGEXP_COUNT\",\"REGEXP_INSTR\",\"REGEXP_REPLACE\",\"REGEXP_SUBSTR\",\"REPEAT\",\"REPLACE\",\"RID\",\"RIGHT\",\"ROUND_TIMESTAMP\",\"ROUND\",\"RPAD\",\"RRN\",\"RTRIM\",\"SCORE\",\"SECOND\",\"SIGN\",\"SIN\",\"SINH\",\"SOUNDEX\",\"SPACE\",\"SQRT\",\"STRIP\",\"STRLEFT\",\"STRPOS\",\"STRRIGHT\",\"SUBSTR\",\"SUBSTRING\",\"TABLE_NAME\",\"TABLE_SCHEMA\",\"TAN\",\"TANH\",\"TIMESTAMP_FORMAT\",\"TIMESTAMP_ISO\",\"TIMESTAMPDIFF_BIG\",\"TIMESTAMPDIFF\",\"TO_CHAR\",\"TO_CLOB\",\"TO_DATE\",\"TO_NUMBER\",\"TO_TIMESTAMP\",\"TOTALORDER\",\"TRANSLATE\",\"TRIM_ARRAY\",\"TRIM\",\"TRUNC_TIMESTAMP\",\"TRUNC\",\"TRUNCATE\",\"UCASE\",\"UPPER\",\"URL_DECODE\",\"URL_ENCODE\",\"VALUE\",\"VARBINARY_FORMAT\",\"VARCHAR_BIT_FORMAT\",\"VARCHAR_FORMAT_BINARY\",\"VARCHAR_FORMAT\",\"VERIFY_GROUP_FOR_USER\",\"WEEK_ISO\",\"WEEK\",\"WRAP\",\"XMLATTRIBUTES\",\"XMLCOMMENT\",\"XMLCONCAT\",\"XMLDOCUMENT\",\"XMLELEMENT\",\"XMLFOREST\",\"XMLNAMESPACES\",\"XMLPARSE\",\"XMLPI\",\"XMLROW\",\"XMLSERIALIZE\",\"XMLTEXT\",\"XMLVALIDATE\",\"XOR\",\"XSLTRANSFORM\",\"YEAR\",\"ZONED\",\"BASE_TABLE\",\"HTTP_DELETE_BLOB_VERBOSE\",\"HTTP_DELETE_VERBOSE\",\"HTTP_GET_BLOB_VERBOSE\",\"HTTP_GET_VERBOSE\",\"HTTP_PATCH_BLOB_VERBOSE\",\"HTTP_PATCH_VERBOSE\",\"HTTP_POST_BLOB_VERBOSE\",\"HTTP_POST_VERBOSE\",\"HTTP_PUT_BLOB_VERBOSE\",\"HTTP_PUT_VERBOSE\",\"JSON_TABLE\",\"MQREADALL\",\"MQREADALLCLOB\",\"MQRECEIVEALL\",\"MQRECEIVEALLCLOB\",\"XMLTABLE\",\"UNPACK\",\"CUME_DIST\",\"DENSE_RANK\",\"FIRST_VALUE\",\"LAG\",\"LAST_VALUE\",\"LEAD\",\"NTH_VALUE\",\"NTILE\",\"PERCENT_RANK\",\"RANK\",\"RATIO_TO_REPORT\",\"ROW_NUMBER\",\"CAST\"],Bs=[\"ABSENT\",\"ACCORDING\",\"ACCTNG\",\"ACTION\",\"ACTIVATE\",\"ADD\",\"ALIAS\",\"ALL\",\"ALLOCATE\",\"ALLOW\",\"ALTER\",\"AND\",\"ANY\",\"APPEND\",\"APPLNAME\",\"ARRAY\",\"ARRAY_AGG\",\"ARRAY_TRIM\",\"AS\",\"ASC\",\"ASENSITIVE\",\"ASSOCIATE\",\"ATOMIC\",\"ATTACH\",\"ATTRIBUTES\",\"AUTHORIZATION\",\"AUTONOMOUS\",\"BEFORE\",\"BEGIN\",\"BETWEEN\",\"BIND\",\"BSON\",\"BUFFERPOOL\",\"BY\",\"CACHE\",\"CALL\",\"CALLED\",\"CARDINALITY\",\"CASE\",\"CAST\",\"CHECK\",\"CL\",\"CLOSE\",\"CLUSTER\",\"COLLECT\",\"COLLECTION\",\"COLUMN\",\"COMMENT\",\"COMMIT\",\"COMPACT\",\"COMPARISONS\",\"COMPRESS\",\"CONCAT\",\"CONCURRENT\",\"CONDITION\",\"CONNECT\",\"CONNECT_BY_ROOT\",\"CONNECTION\",\"CONSTANT\",\"CONSTRAINT\",\"CONTAINS\",\"CONTENT\",\"CONTINUE\",\"COPY\",\"COUNT\",\"COUNT_BIG\",\"CREATE\",\"CREATEIN\",\"CROSS\",\"CUBE\",\"CUME_DIST\",\"CURRENT\",\"CURRENT_DATE\",\"CURRENT_PATH\",\"CURRENT_SCHEMA\",\"CURRENT_SERVER\",\"CURRENT_TIME\",\"CURRENT_TIMESTAMP\",\"CURRENT_TIMEZONE\",\"CURRENT_USER\",\"CURSOR\",\"CYCLE\",\"DATABASE\",\"DATAPARTITIONNAME\",\"DATAPARTITIONNUM\",\"DAY\",\"DAYS\",\"DB2GENERAL\",\"DB2GENRL\",\"DB2SQL\",\"DBINFO\",\"DBPARTITIONNAME\",\"DBPARTITIONNUM\",\"DEACTIVATE\",\"DEALLOCATE\",\"DECLARE\",\"DEFAULT\",\"DEFAULTS\",\"DEFER\",\"DEFINE\",\"DEFINITION\",\"DELETE\",\"DELETING\",\"DENSE_RANK\",\"DENSERANK\",\"DESC\",\"DESCRIBE\",\"DESCRIPTOR\",\"DETACH\",\"DETERMINISTIC\",\"DIAGNOSTICS\",\"DISABLE\",\"DISALLOW\",\"DISCONNECT\",\"DISTINCT\",\"DO\",\"DOCUMENT\",\"DROP\",\"DYNAMIC\",\"EACH\",\"ELSE\",\"ELSEIF\",\"EMPTY\",\"ENABLE\",\"ENCODING\",\"ENCRYPTION\",\"END\",\"END-EXEC\",\"ENDING\",\"ENFORCED\",\"ERROR\",\"ESCAPE\",\"EVERY\",\"EXCEPT\",\"EXCEPTION\",\"EXCLUDING\",\"EXCLUSIVE\",\"EXECUTE\",\"EXISTS\",\"EXIT\",\"EXTEND\",\"EXTERNAL\",\"EXTRACT\",\"FALSE\",\"FENCED\",\"FETCH\",\"FIELDPROC\",\"FILE\",\"FINAL\",\"FIRST_VALUE\",\"FOR\",\"FOREIGN\",\"FORMAT\",\"FREE\",\"FREEPAGE\",\"FROM\",\"FULL\",\"FUNCTION\",\"GBPCACHE\",\"GENERAL\",\"GENERATED\",\"GET\",\"GLOBAL\",\"GO\",\"GOTO\",\"GRANT\",\"GROUP\",\"HANDLER\",\"HASH\",\"HASH_ROW\",\"HASHED_VALUE\",\"HAVING\",\"HINT\",\"HOLD\",\"HOUR\",\"HOURS\",\"IDENTITY\",\"IF\",\"IGNORE\",\"IMMEDIATE\",\"IMPLICITLY\",\"IN\",\"INCLUDE\",\"INCLUDING\",\"INCLUSIVE\",\"INCREMENT\",\"INDEX\",\"INDEXBP\",\"INDICATOR\",\"INF\",\"INFINITY\",\"INHERIT\",\"INLINE\",\"INNER\",\"INOUT\",\"INSENSITIVE\",\"INSERT\",\"INSERTING\",\"INTEGRITY\",\"INTERPRET\",\"INTERSECT\",\"INTO\",\"IS\",\"ISNULL\",\"ISOLATION\",\"ITERATE\",\"JAVA\",\"JOIN\",\"JSON\",\"JSON_ARRAY\",\"JSON_ARRAYAGG\",\"JSON_EXISTS\",\"JSON_OBJECT\",\"JSON_OBJECTAGG\",\"JSON_QUERY\",\"JSON_TABLE\",\"JSON_VALUE\",\"KEEP\",\"KEY\",\"KEYS\",\"LABEL\",\"LAG\",\"LANGUAGE\",\"LAST_VALUE\",\"LATERAL\",\"LEAD\",\"LEAVE\",\"LEFT\",\"LEVEL2\",\"LIKE\",\"LIMIT\",\"LINKTYPE\",\"LISTAGG\",\"LOCAL\",\"LOCALDATE\",\"LOCALTIME\",\"LOCALTIMESTAMP\",\"LOCATION\",\"LOCATOR\",\"LOCK\",\"LOCKSIZE\",\"LOG\",\"LOGGED\",\"LOOP\",\"MAINTAINED\",\"MASK\",\"MATCHED\",\"MATERIALIZED\",\"MAXVALUE\",\"MERGE\",\"MICROSECOND\",\"MICROSECONDS\",\"MINPCTUSED\",\"MINUTE\",\"MINUTES\",\"MINVALUE\",\"MIRROR\",\"MIXED\",\"MODE\",\"MODIFIES\",\"MONTH\",\"MONTHS\",\"NAMESPACE\",\"NAN\",\"NATIONAL\",\"NCHAR\",\"NCLOB\",\"NESTED\",\"NEW\",\"NEW_TABLE\",\"NEXTVAL\",\"NO\",\"NOCACHE\",\"NOCYCLE\",\"NODENAME\",\"NODENUMBER\",\"NOMAXVALUE\",\"NOMINVALUE\",\"NONE\",\"NOORDER\",\"NORMALIZED\",\"NOT\",\"NOTNULL\",\"NTH_VALUE\",\"NTILE\",\"NULL\",\"NULLS\",\"NVARCHAR\",\"OBID\",\"OBJECT\",\"OF\",\"OFF\",\"OFFSET\",\"OLD\",\"OLD_TABLE\",\"OMIT\",\"ON\",\"ONLY\",\"OPEN\",\"OPTIMIZE\",\"OPTION\",\"OR\",\"ORDER\",\"ORDINALITY\",\"ORGANIZE\",\"OUT\",\"OUTER\",\"OVER\",\"OVERLAY\",\"OVERRIDING\",\"PACKAGE\",\"PADDED\",\"PAGE\",\"PAGESIZE\",\"PARAMETER\",\"PART\",\"PARTITION\",\"PARTITIONED\",\"PARTITIONING\",\"PARTITIONS\",\"PASSING\",\"PASSWORD\",\"PATH\",\"PCTFREE\",\"PERCENT_RANK\",\"PERCENTILE_CONT\",\"PERCENTILE_DISC\",\"PERIOD\",\"PERMISSION\",\"PIECESIZE\",\"PIPE\",\"PLAN\",\"POSITION\",\"PREPARE\",\"PREVVAL\",\"PRIMARY\",\"PRIOR\",\"PRIQTY\",\"PRIVILEGES\",\"PROCEDURE\",\"PROGRAM\",\"PROGRAMID\",\"QUERY\",\"RANGE\",\"RANK\",\"RATIO_TO_REPORT\",\"RCDFMT\",\"READ\",\"READS\",\"RECOVERY\",\"REFERENCES\",\"REFERENCING\",\"REFRESH\",\"REGEXP_LIKE\",\"RELEASE\",\"RENAME\",\"REPEAT\",\"RESET\",\"RESIGNAL\",\"RESTART\",\"RESULT\",\"RESULT_SET_LOCATOR\",\"RETURN\",\"RETURNING\",\"RETURNS\",\"REVOKE\",\"RID\",\"RIGHT\",\"ROLLBACK\",\"ROLLUP\",\"ROUTINE\",\"ROW\",\"ROW_NUMBER\",\"ROWNUMBER\",\"ROWS\",\"RRN\",\"RUN\",\"SAVEPOINT\",\"SBCS\",\"SCALAR\",\"SCHEMA\",\"SCRATCHPAD\",\"SCROLL\",\"SEARCH\",\"SECOND\",\"SECONDS\",\"SECQTY\",\"SECURED\",\"SELECT\",\"SENSITIVE\",\"SEQUENCE\",\"SESSION\",\"SESSION_USER\",\"SET\",\"SIGNAL\",\"SIMPLE\",\"SKIP\",\"SNAN\",\"SOME\",\"SOURCE\",\"SPECIFIC\",\"SQL\",\"SQLID\",\"SQLIND_DEFAULT\",\"SQLIND_UNASSIGNED\",\"STACKED\",\"START\",\"STARTING\",\"STATEMENT\",\"STATIC\",\"STOGROUP\",\"SUBSTRING\",\"SUMMARY\",\"SYNONYM\",\"SYSTEM_TIME\",\"SYSTEM_USER\",\"TABLE\",\"TABLESPACE\",\"TABLESPACES\",\"TAG\",\"THEN\",\"THREADSAFE\",\"TO\",\"TRANSACTION\",\"TRANSFER\",\"TRIGGER\",\"TRIM\",\"TRIM_ARRAY\",\"TRUE\",\"TRUNCATE\",\"TRY_CAST\",\"TYPE\",\"UNDO\",\"UNION\",\"UNIQUE\",\"UNIT\",\"UNKNOWN\",\"UNNEST\",\"UNTIL\",\"UPDATE\",\"UPDATING\",\"URI\",\"USAGE\",\"USE\",\"USER\",\"USERID\",\"USING\",\"VALUE\",\"VALUES\",\"VARIABLE\",\"VARIANT\",\"VCAT\",\"VERSION\",\"VERSIONING\",\"VIEW\",\"VOLATILE\",\"WAIT\",\"WHEN\",\"WHENEVER\",\"WHERE\",\"WHILE\",\"WITH\",\"WITHIN\",\"WITHOUT\",\"WRAPPED\",\"WRAPPER\",\"WRITE\",\"WRKSTNNAME\",\"XMLAGG\",\"XMLATTRIBUTES\",\"XMLCAST\",\"XMLCOMMENT\",\"XMLCONCAT\",\"XMLDOCUMENT\",\"XMLELEMENT\",\"XMLFOREST\",\"XMLGROUP\",\"XMLNAMESPACES\",\"XMLPARSE\",\"XMLPI\",\"XMLROW\",\"XMLSERIALIZE\",\"XMLTABLE\",\"XMLTEXT\",\"XMLVALIDATE\",\"XSLTRANSFORM\",\"XSROBJECT\",\"YEAR\",\"YEARS\",\"YES\",\"ZONE\"],vs=[\"ARRAY\",\"BIGINT\",\"BINARY\",\"BIT\",\"BLOB\",\"BOOLEAN\",\"CCSID\",\"CHAR\",\"CHARACTER\",\"CLOB\",\"DATA\",\"DATALINK\",\"DATE\",\"DBCLOB\",\"DECFLOAT\",\"DECIMAL\",\"DEC\",\"DOUBLE\",\"DOUBLE PRECISION\",\"FLOAT\",\"GRAPHIC\",\"INT\",\"INTEGER\",\"LONG\",\"NUMERIC\",\"REAL\",\"ROWID\",\"SMALLINT\",\"TIME\",\"TIMESTAMP\",\"VARBINARY\",\"VARCHAR\",\"VARGRAPHIC\",\"XML\"],Fs=v([\"SELECT [ALL | DISTINCT]\"]),Ys=v([\"WITH [RECURSIVE]\",\"INTO\",\"FROM\",\"WHERE\",\"GROUP BY\",\"HAVING\",\"PARTITION BY\",\"ORDER [SIBLINGS] BY [INPUT SEQUENCE]\",\"LIMIT\",\"OFFSET\",\"FETCH {FIRST | NEXT}\",\"FOR UPDATE [OF]\",\"FOR READ ONLY\",\"OPTIMIZE FOR\",\"INSERT INTO\",\"VALUES\",\"SET\",\"MERGE INTO\",\"WHEN [NOT] MATCHED [THEN]\",\"UPDATE SET\",\"DELETE\",\"INSERT\",\"FOR SYSTEM NAME\"]),Rr=v([\"CREATE [OR REPLACE] TABLE\"]),kt=v([\"CREATE [OR REPLACE] [RECURSIVE] VIEW\",\"UPDATE\",\"WHERE CURRENT OF\",\"WITH {NC | RR | RS | CS | UR}\",\"DELETE FROM\",\"DROP TABLE\",\"ALTER TABLE\",\"ADD [COLUMN]\",\"ALTER [COLUMN]\",\"DROP [COLUMN]\",\"SET DATA TYPE\",\"SET {GENERATED ALWAYS | GENERATED BY DEFAULT}\",\"SET NOT NULL\",\"SET {NOT HIDDEN | IMPLICITLY HIDDEN}\",\"SET FIELDPROC\",\"DROP {DEFAULT | NOT NULL | GENERATED | IDENTITY | ROW CHANGE TIMESTAMP | FIELDPROC}\",\"TRUNCATE [TABLE]\",\"SET [CURRENT] SCHEMA\",\"SET CURRENT_SCHEMA\",\"ALLOCATE CURSOR\",\"ALLOCATE [SQL] DESCRIPTOR [LOCAL | GLOBAL] SQL\",\"ALTER [SPECIFIC] {FUNCTION | PROCEDURE}\",\"ALTER {MASK | PERMISSION | SEQUENCE | TRIGGER}\",\"ASSOCIATE [RESULT SET] {LOCATOR | LOCATORS}\",\"BEGIN DECLARE SECTION\",\"CALL\",\"CLOSE\",\"COMMENT ON {ALIAS | COLUMN | CONSTRAINT | INDEX | MASK | PACKAGE | PARAMETER | PERMISSION | SEQUENCE | TABLE | TRIGGER | VARIABLE | XSROBJECT}\",\"COMMENT ON [SPECIFIC] {FUNCTION | PROCEDURE | ROUTINE}\",\"COMMENT ON PARAMETER SPECIFIC {FUNCTION | PROCEDURE | ROUTINE}\",\"COMMENT ON [TABLE FUNCTION] RETURN COLUMN\",\"COMMENT ON [TABLE FUNCTION] RETURN COLUMN SPECIFIC [PROCEDURE | ROUTINE]\",\"COMMIT [WORK] [HOLD]\",\"CONNECT [TO | RESET] USER\",\"CREATE [OR REPLACE] {ALIAS | FUNCTION | MASK | PERMISSION | PROCEDURE | SEQUENCE | TRIGGER | VARIABLE}\",\"CREATE [ENCODED VECTOR] INDEX\",\"CREATE UNIQUE [WHERE NOT NULL] INDEX\",\"CREATE SCHEMA\",\"CREATE TYPE\",\"DEALLOCATE [SQL] DESCRIPTOR [LOCAL | GLOBAL]\",\"DECLARE CURSOR\",\"DECLARE GLOBAL TEMPORARY TABLE\",\"DECLARE\",\"DESCRIBE CURSOR\",\"DESCRIBE INPUT\",\"DESCRIBE [OUTPUT]\",\"DESCRIBE {PROCEDURE | ROUTINE}\",\"DESCRIBE TABLE\",\"DISCONNECT ALL [SQL]\",\"DISCONNECT [CURRENT]\",\"DROP {ALIAS | INDEX | MASK | PACKAGE | PERMISSION | SCHEMA | SEQUENCE | TABLE | TYPE | VARIABLE | XSROBJECT} [IF EXISTS]\",\"DROP [SPECIFIC] {FUNCTION | PROCEDURE | ROUTINE} [IF EXISTS]\",\"END DECLARE SECTION\",\"EXECUTE [IMMEDIATE]\",\"FREE LOCATOR\",\"GET [SQL] DESCRIPTOR [LOCAL | GLOBAL]\",\"GET [CURRENT | STACKED] DIAGNOSTICS\",\"GRANT {ALL [PRIVILEGES] | ALTER | EXECUTE} ON {FUNCTION | PROCEDURE | ROUTINE | PACKAGE | SCHEMA | SEQUENCE | TABLE | TYPE | VARIABLE | XSROBJECT}\",\"HOLD LOCATOR\",\"INCLUDE\",\"LABEL ON {ALIAS | COLUMN | CONSTRAINT | INDEX | MASK | PACKAGE | PERMISSION | SEQUENCE | TABLE | TRIGGER | VARIABLE | XSROBJECT}\",\"LABEL ON [SPECIFIC] {FUNCTION | PROCEDURE | ROUTINE}\",\"LOCK TABLE\",\"OPEN\",\"PREPARE\",\"REFRESH TABLE\",\"RELEASE\",\"RELEASE [TO] SAVEPOINT\",\"RENAME [TABLE | INDEX] TO\",\"REVOKE {ALL [PRIVILEGES] | ALTER | EXECUTE} ON {FUNCTION | PROCEDURE | ROUTINE | PACKAGE | SCHEMA | SEQUENCE | TABLE | TYPE | VARIABLE | XSROBJECT}\",\"ROLLBACK [WORK] [HOLD | TO SAVEPOINT]\",\"SAVEPOINT\",\"SET CONNECTION\",\"SET CURRENT {DEBUG MODE | DECFLOAT ROUNDING MODE | DEGREE | IMPLICIT XMLPARSE OPTION | TEMPORAL SYSTEM_TIME}\",\"SET [SQL] DESCRIPTOR [LOCAL | GLOBAL]\",\"SET ENCRYPTION PASSWORD\",\"SET OPTION\",\"SET {[CURRENT [FUNCTION]] PATH | CURRENT_PATH}\",\"SET RESULT SETS [WITH RETURN [TO CALLER | TO CLIENT]]\",\"SET SESSION AUTHORIZATION\",\"SET SESSION_USER\",\"SET TRANSACTION\",\"SIGNAL SQLSTATE [VALUE]\",\"TAG\",\"TRANSFER OWNERSHIP OF\",\"WHENEVER {NOT FOUND | SQLERROR | SQLWARNING}\"]),Vs=v([\"UNION [ALL]\",\"EXCEPT [ALL]\",\"INTERSECT [ALL]\"]),Ws=v([\"JOIN\",\"{LEFT | RIGHT | FULL} [OUTER] JOIN\",\"[LEFT | RIGHT] EXCEPTION JOIN\",\"{INNER | CROSS} JOIN\"]),ws=v([\"ON DELETE\",\"ON UPDATE\",\"SET NULL\",\"{ROWS | RANGE} BETWEEN\"]),$s={name:\"db2i\",tokenizerOptions:{reservedSelect:Fs,reservedClauses:[...Ys,...Rr,...kt],reservedSetOperations:Vs,reservedJoins:Ws,reservedPhrases:ws,reservedKeywords:Bs,reservedDataTypes:vs,reservedFunctionNames:ys,nestedBlockComments:!0,extraParens:[\"[]\"],stringTypes:[{quote:\"''-qq\",prefixes:[\"G\",\"N\"]},{quote:\"''-raw\",prefixes:[\"X\",\"BX\",\"GX\",\"UX\"],requirePrefix:!0}],identTypes:['\"\"-qq'],identChars:{first:\"@#$\",rest:\"@#$\"},paramTypes:{positional:!0,named:[\":\"]},paramChars:{first:\"@#$\",rest:\"@#$\"},operators:[\"**\",\"¬=\",\"¬>\",\"¬<\",\"!>\",\"!<\",\"||\",\"=>\"]},formatOptions:{onelineClauses:[...Rr,...kt],tabularOnelineClauses:kt}},xs=[\"ABS\",\"ACOS\",\"ASIN\",\"ATAN\",\"BIN\",\"BROUND\",\"CBRT\",\"CEIL\",\"CEILING\",\"CONV\",\"COS\",\"DEGREES\",\"EXP\",\"FACTORIAL\",\"FLOOR\",\"GREATEST\",\"HEX\",\"LEAST\",\"LN\",\"LOG\",\"LOG10\",\"LOG2\",\"NEGATIVE\",\"PI\",\"PMOD\",\"POSITIVE\",\"POW\",\"POWER\",\"RADIANS\",\"RAND\",\"ROUND\",\"SHIFTLEFT\",\"SHIFTRIGHT\",\"SHIFTRIGHTUNSIGNED\",\"SIGN\",\"SIN\",\"SQRT\",\"TAN\",\"UNHEX\",\"WIDTH_BUCKET\",\"ARRAY_CONTAINS\",\"MAP_KEYS\",\"MAP_VALUES\",\"SIZE\",\"SORT_ARRAY\",\"BINARY\",\"CAST\",\"ADD_MONTHS\",\"DATE\",\"DATE_ADD\",\"DATE_FORMAT\",\"DATE_SUB\",\"DATEDIFF\",\"DAY\",\"DAYNAME\",\"DAYOFMONTH\",\"DAYOFYEAR\",\"EXTRACT\",\"FROM_UNIXTIME\",\"FROM_UTC_TIMESTAMP\",\"HOUR\",\"LAST_DAY\",\"MINUTE\",\"MONTH\",\"MONTHS_BETWEEN\",\"NEXT_DAY\",\"QUARTER\",\"SECOND\",\"TIMESTAMP\",\"TO_DATE\",\"TO_UTC_TIMESTAMP\",\"TRUNC\",\"UNIX_TIMESTAMP\",\"WEEKOFYEAR\",\"YEAR\",\"ASSERT_TRUE\",\"COALESCE\",\"IF\",\"ISNOTNULL\",\"ISNULL\",\"NULLIF\",\"NVL\",\"ASCII\",\"BASE64\",\"CHARACTER_LENGTH\",\"CHR\",\"CONCAT\",\"CONCAT_WS\",\"CONTEXT_NGRAMS\",\"DECODE\",\"ELT\",\"ENCODE\",\"FIELD\",\"FIND_IN_SET\",\"FORMAT_NUMBER\",\"GET_JSON_OBJECT\",\"IN_FILE\",\"INITCAP\",\"INSTR\",\"LCASE\",\"LENGTH\",\"LEVENSHTEIN\",\"LOCATE\",\"LOWER\",\"LPAD\",\"LTRIM\",\"NGRAMS\",\"OCTET_LENGTH\",\"PARSE_URL\",\"PRINTF\",\"QUOTE\",\"REGEXP_EXTRACT\",\"REGEXP_REPLACE\",\"REPEAT\",\"REVERSE\",\"RPAD\",\"RTRIM\",\"SENTENCES\",\"SOUNDEX\",\"SPACE\",\"SPLIT\",\"STR_TO_MAP\",\"SUBSTR\",\"SUBSTRING\",\"TRANSLATE\",\"TRIM\",\"UCASE\",\"UNBASE64\",\"UPPER\",\"MASK\",\"MASK_FIRST_N\",\"MASK_HASH\",\"MASK_LAST_N\",\"MASK_SHOW_FIRST_N\",\"MASK_SHOW_LAST_N\",\"AES_DECRYPT\",\"AES_ENCRYPT\",\"CRC32\",\"CURRENT_DATABASE\",\"CURRENT_USER\",\"HASH\",\"JAVA_METHOD\",\"LOGGED_IN_USER\",\"MD5\",\"REFLECT\",\"SHA\",\"SHA1\",\"SHA2\",\"SURROGATE_KEY\",\"VERSION\",\"AVG\",\"COLLECT_LIST\",\"COLLECT_SET\",\"CORR\",\"COUNT\",\"COVAR_POP\",\"COVAR_SAMP\",\"HISTOGRAM_NUMERIC\",\"MAX\",\"MIN\",\"NTILE\",\"PERCENTILE\",\"PERCENTILE_APPROX\",\"REGR_AVGX\",\"REGR_AVGY\",\"REGR_COUNT\",\"REGR_INTERCEPT\",\"REGR_R2\",\"REGR_SLOPE\",\"REGR_SXX\",\"REGR_SXY\",\"REGR_SYY\",\"STDDEV_POP\",\"STDDEV_SAMP\",\"SUM\",\"VAR_POP\",\"VAR_SAMP\",\"VARIANCE\",\"EXPLODE\",\"INLINE\",\"JSON_TUPLE\",\"PARSE_URL_TUPLE\",\"POSEXPLODE\",\"STACK\",\"LEAD\",\"LAG\",\"FIRST_VALUE\",\"LAST_VALUE\",\"RANK\",\"ROW_NUMBER\",\"DENSE_RANK\",\"CUME_DIST\",\"PERCENT_RANK\",\"NTILE\"],Xs=[\"ADD\",\"ADMIN\",\"AFTER\",\"ANALYZE\",\"ARCHIVE\",\"ASC\",\"BEFORE\",\"BUCKET\",\"BUCKETS\",\"CASCADE\",\"CHANGE\",\"CLUSTER\",\"CLUSTERED\",\"CLUSTERSTATUS\",\"COLLECTION\",\"COLUMNS\",\"COMMENT\",\"COMPACT\",\"COMPACTIONS\",\"COMPUTE\",\"CONCATENATE\",\"CONTINUE\",\"DATA\",\"DATABASES\",\"DATETIME\",\"DAY\",\"DBPROPERTIES\",\"DEFERRED\",\"DEFINED\",\"DELIMITED\",\"DEPENDENCY\",\"DESC\",\"DIRECTORIES\",\"DIRECTORY\",\"DISABLE\",\"DISTRIBUTE\",\"ELEM_TYPE\",\"ENABLE\",\"ESCAPED\",\"EXCLUSIVE\",\"EXPLAIN\",\"EXPORT\",\"FIELDS\",\"FILE\",\"FILEFORMAT\",\"FIRST\",\"FORMAT\",\"FORMATTED\",\"FUNCTIONS\",\"HOLD_DDLTIME\",\"HOUR\",\"IDXPROPERTIES\",\"IGNORE\",\"INDEX\",\"INDEXES\",\"INPATH\",\"INPUTDRIVER\",\"INPUTFORMAT\",\"ITEMS\",\"JAR\",\"KEYS\",\"KEY_TYPE\",\"LIMIT\",\"LINES\",\"LOAD\",\"LOCATION\",\"LOCK\",\"LOCKS\",\"LOGICAL\",\"LONG\",\"MAPJOIN\",\"MATERIALIZED\",\"METADATA\",\"MINUS\",\"MINUTE\",\"MONTH\",\"MSCK\",\"NOSCAN\",\"NO_DROP\",\"OFFLINE\",\"OPTION\",\"OUTPUTDRIVER\",\"OUTPUTFORMAT\",\"OVERWRITE\",\"OWNER\",\"PARTITIONED\",\"PARTITIONS\",\"PLUS\",\"PRETTY\",\"PRINCIPALS\",\"PROTECTION\",\"PURGE\",\"READ\",\"READONLY\",\"REBUILD\",\"RECORDREADER\",\"RECORDWRITER\",\"RELOAD\",\"RENAME\",\"REPAIR\",\"REPLACE\",\"REPLICATION\",\"RESTRICT\",\"REWRITE\",\"ROLE\",\"ROLES\",\"SCHEMA\",\"SCHEMAS\",\"SECOND\",\"SEMI\",\"SERDE\",\"SERDEPROPERTIES\",\"SERVER\",\"SETS\",\"SHARED\",\"SHOW\",\"SHOW_DATABASE\",\"SKEWED\",\"SORT\",\"SORTED\",\"SSL\",\"STATISTICS\",\"STORED\",\"STREAMTABLE\",\"STRING\",\"TABLES\",\"TBLPROPERTIES\",\"TEMPORARY\",\"TERMINATED\",\"TINYINT\",\"TOUCH\",\"TRANSACTIONS\",\"UNARCHIVE\",\"UNDO\",\"UNIONTYPE\",\"UNLOCK\",\"UNSET\",\"UNSIGNED\",\"URI\",\"USE\",\"UTC\",\"UTCTIMESTAMP\",\"VALUE_TYPE\",\"VIEW\",\"WHILE\",\"YEAR\",\"AUTOCOMMIT\",\"ISOLATION\",\"LEVEL\",\"OFFSET\",\"SNAPSHOT\",\"TRANSACTION\",\"WORK\",\"WRITE\",\"ABORT\",\"KEY\",\"LAST\",\"NORELY\",\"NOVALIDATE\",\"NULLS\",\"RELY\",\"VALIDATE\",\"DETAIL\",\"DOW\",\"EXPRESSION\",\"OPERATOR\",\"QUARTER\",\"SUMMARY\",\"VECTORIZATION\",\"WEEK\",\"YEARS\",\"MONTHS\",\"WEEKS\",\"DAYS\",\"HOURS\",\"MINUTES\",\"SECONDS\",\"TIMESTAMPTZ\",\"ZONE\",\"ALL\",\"ALTER\",\"AND\",\"AS\",\"AUTHORIZATION\",\"BETWEEN\",\"BOTH\",\"BY\",\"CASE\",\"CAST\",\"COLUMN\",\"CONF\",\"CREATE\",\"CROSS\",\"CUBE\",\"CURRENT\",\"CURRENT_DATE\",\"CURRENT_TIMESTAMP\",\"CURSOR\",\"DATABASE\",\"DELETE\",\"DESCRIBE\",\"DISTINCT\",\"DROP\",\"ELSE\",\"END\",\"EXCHANGE\",\"EXISTS\",\"EXTENDED\",\"EXTERNAL\",\"FALSE\",\"FETCH\",\"FOLLOWING\",\"FOR\",\"FROM\",\"FULL\",\"FUNCTION\",\"GRANT\",\"GROUP\",\"GROUPING\",\"HAVING\",\"IF\",\"IMPORT\",\"IN\",\"INNER\",\"INSERT\",\"INTERSECT\",\"INTO\",\"IS\",\"JOIN\",\"LATERAL\",\"LEFT\",\"LESS\",\"LIKE\",\"LOCAL\",\"MACRO\",\"MORE\",\"NONE\",\"NOT\",\"NULL\",\"OF\",\"ON\",\"OR\",\"ORDER\",\"OUT\",\"OUTER\",\"OVER\",\"PARTIALSCAN\",\"PARTITION\",\"PERCENT\",\"PRECEDING\",\"PRESERVE\",\"PROCEDURE\",\"RANGE\",\"READS\",\"REDUCE\",\"REVOKE\",\"RIGHT\",\"ROLLUP\",\"ROW\",\"ROWS\",\"SELECT\",\"SET\",\"TABLE\",\"TABLESAMPLE\",\"THEN\",\"TO\",\"TRANSFORM\",\"TRIGGER\",\"TRUE\",\"TRUNCATE\",\"UNBOUNDED\",\"UNION\",\"UNIQUEJOIN\",\"UPDATE\",\"USER\",\"USING\",\"UTC_TMESTAMP\",\"VALUES\",\"WHEN\",\"WHERE\",\"WINDOW\",\"WITH\",\"COMMIT\",\"ONLY\",\"REGEXP\",\"RLIKE\",\"ROLLBACK\",\"START\",\"CACHE\",\"CONSTRAINT\",\"FOREIGN\",\"PRIMARY\",\"REFERENCES\",\"DAYOFWEEK\",\"EXTRACT\",\"FLOOR\",\"VIEWS\",\"TIME\",\"SYNC\",\"TEXTFILE\",\"SEQUENCEFILE\",\"ORC\",\"CSV\",\"TSV\",\"PARQUET\",\"AVRO\",\"RCFILE\",\"JSONFILE\",\"INPUTFORMAT\",\"OUTPUTFORMAT\"],ks=[\"ARRAY\",\"BIGINT\",\"BINARY\",\"BOOLEAN\",\"CHAR\",\"DATE\",\"DECIMAL\",\"DOUBLE\",\"FLOAT\",\"INT\",\"INTEGER\",\"INTERVAL\",\"MAP\",\"NUMERIC\",\"PRECISION\",\"SMALLINT\",\"STRUCT\",\"TIMESTAMP\",\"VARCHAR\"],Ks=v([\"SELECT [ALL | DISTINCT]\"]),Js=v([\"WITH\",\"FROM\",\"WHERE\",\"GROUP BY\",\"HAVING\",\"WINDOW\",\"PARTITION BY\",\"ORDER BY\",\"SORT BY\",\"CLUSTER BY\",\"DISTRIBUTE BY\",\"LIMIT\",\"INSERT INTO [TABLE]\",\"VALUES\",\"SET\",\"MERGE INTO\",\"WHEN [NOT] MATCHED [THEN]\",\"UPDATE SET\",\"INSERT [VALUES]\",\"INSERT OVERWRITE [LOCAL] DIRECTORY\",\"LOAD DATA [LOCAL] INPATH\",\"[OVERWRITE] INTO TABLE\"]),nr=v([\"CREATE [TEMPORARY] [EXTERNAL] TABLE [IF NOT EXISTS]\"]),Kt=v([\"CREATE [MATERIALIZED] VIEW [IF NOT EXISTS]\",\"UPDATE\",\"DELETE FROM\",\"DROP TABLE [IF EXISTS]\",\"ALTER TABLE\",\"RENAME TO\",\"TRUNCATE [TABLE]\",\"ALTER\",\"CREATE\",\"USE\",\"DESCRIBE\",\"DROP\",\"FETCH\",\"SHOW\",\"STORED AS\",\"STORED BY\",\"ROW FORMAT\"]),qs=v([\"UNION [ALL | DISTINCT]\"]),Qs=v([\"JOIN\",\"{LEFT | RIGHT | FULL} [OUTER] JOIN\",\"{INNER | CROSS} JOIN\",\"LEFT SEMI JOIN\"]),Zs=v([\"{ROWS | RANGE} BETWEEN\"]),js={name:\"hive\",tokenizerOptions:{reservedSelect:Ks,reservedClauses:[...Js,...nr,...Kt],reservedSetOperations:qs,reservedJoins:Qs,reservedPhrases:Zs,reservedKeywords:Xs,reservedDataTypes:ks,reservedFunctionNames:xs,extraParens:[\"[]\"],stringTypes:['\"\"-bs',\"''-bs\"],identTypes:[\"``\"],variableTypes:[{quote:\"{}\",prefixes:[\"$\"],requirePrefix:!0}],operators:[\"%\",\"~\",\"^\",\"|\",\"&\",\"<=>\",\"==\",\"!\",\"||\"]},formatOptions:{onelineClauses:[...nr,...Kt],tabularOnelineClauses:Kt}};function yt(E){return E.map((e,T)=>{const t=E[T+1]||tt;if(FE.SET(e)&&t.text===\"(\")return AE(EE({},e),{type:\"RESERVED_FUNCTION_NAME\"});const r=E[T-1]||tt;return FE.VALUES(e)&&r.text===\"=\"?AE(EE({},e),{type:\"RESERVED_FUNCTION_NAME\"}):e})}var zs=[\"ACCESSIBLE\",\"ADD\",\"ALL\",\"ALTER\",\"ANALYZE\",\"AND\",\"AS\",\"ASC\",\"ASENSITIVE\",\"BEFORE\",\"BETWEEN\",\"BOTH\",\"BY\",\"CALL\",\"CASCADE\",\"CASE\",\"CHANGE\",\"CHECK\",\"COLLATE\",\"COLUMN\",\"CONDITION\",\"CONSTRAINT\",\"CONTINUE\",\"CONVERT\",\"CREATE\",\"CROSS\",\"CURRENT_DATE\",\"CURRENT_ROLE\",\"CURRENT_TIME\",\"CURRENT_TIMESTAMP\",\"CURRENT_USER\",\"CURSOR\",\"DATABASE\",\"DATABASES\",\"DAY_HOUR\",\"DAY_MICROSECOND\",\"DAY_MINUTE\",\"DAY_SECOND\",\"DECLARE\",\"DEFAULT\",\"DELAYED\",\"DELETE\",\"DELETE_DOMAIN_ID\",\"DESC\",\"DESCRIBE\",\"DETERMINISTIC\",\"DISTINCT\",\"DISTINCTROW\",\"DIV\",\"DO_DOMAIN_IDS\",\"DROP\",\"DUAL\",\"EACH\",\"ELSE\",\"ELSEIF\",\"ENCLOSED\",\"ESCAPED\",\"EXCEPT\",\"EXISTS\",\"EXIT\",\"EXPLAIN\",\"FALSE\",\"FETCH\",\"FOR\",\"FORCE\",\"FOREIGN\",\"FROM\",\"FULLTEXT\",\"GENERAL\",\"GRANT\",\"GROUP\",\"HAVING\",\"HIGH_PRIORITY\",\"HOUR_MICROSECOND\",\"HOUR_MINUTE\",\"HOUR_SECOND\",\"IF\",\"IGNORE\",\"IGNORE_DOMAIN_IDS\",\"IGNORE_SERVER_IDS\",\"IN\",\"INDEX\",\"INFILE\",\"INNER\",\"INOUT\",\"INSENSITIVE\",\"INSERT\",\"INTERSECT\",\"INTERVAL\",\"INTO\",\"IS\",\"ITERATE\",\"JOIN\",\"KEY\",\"KEYS\",\"KILL\",\"LEADING\",\"LEAVE\",\"LEFT\",\"LIKE\",\"LIMIT\",\"LINEAR\",\"LINES\",\"LOAD\",\"LOCALTIME\",\"LOCALTIMESTAMP\",\"LOCK\",\"LOOP\",\"LOW_PRIORITY\",\"MASTER_HEARTBEAT_PERIOD\",\"MASTER_SSL_VERIFY_SERVER_CERT\",\"MATCH\",\"MAXVALUE\",\"MINUTE_MICROSECOND\",\"MINUTE_SECOND\",\"MOD\",\"MODIFIES\",\"NATURAL\",\"NOT\",\"NO_WRITE_TO_BINLOG\",\"NULL\",\"OFFSET\",\"ON\",\"OPTIMIZE\",\"OPTION\",\"OPTIONALLY\",\"OR\",\"ORDER\",\"OUT\",\"OUTER\",\"OUTFILE\",\"OVER\",\"PAGE_CHECKSUM\",\"PARSE_VCOL_EXPR\",\"PARTITION\",\"POSITION\",\"PRIMARY\",\"PROCEDURE\",\"PURGE\",\"RANGE\",\"READ\",\"READS\",\"READ_WRITE\",\"RECURSIVE\",\"REF_SYSTEM_ID\",\"REFERENCES\",\"REGEXP\",\"RELEASE\",\"RENAME\",\"REPEAT\",\"REPLACE\",\"REQUIRE\",\"RESIGNAL\",\"RESTRICT\",\"RETURN\",\"RETURNING\",\"REVOKE\",\"RIGHT\",\"RLIKE\",\"ROW_NUMBER\",\"ROWS\",\"SCHEMA\",\"SCHEMAS\",\"SECOND_MICROSECOND\",\"SELECT\",\"SENSITIVE\",\"SEPARATOR\",\"SET\",\"SHOW\",\"SIGNAL\",\"SLOW\",\"SPATIAL\",\"SPECIFIC\",\"SQL\",\"SQLEXCEPTION\",\"SQLSTATE\",\"SQLWARNING\",\"SQL_BIG_RESULT\",\"SQL_CALC_FOUND_ROWS\",\"SQL_SMALL_RESULT\",\"SSL\",\"STARTING\",\"STATS_AUTO_RECALC\",\"STATS_PERSISTENT\",\"STATS_SAMPLE_PAGES\",\"STRAIGHT_JOIN\",\"TABLE\",\"TERMINATED\",\"THEN\",\"TO\",\"TRAILING\",\"TRIGGER\",\"TRUE\",\"UNDO\",\"UNION\",\"UNIQUE\",\"UNLOCK\",\"UNSIGNED\",\"UPDATE\",\"USAGE\",\"USE\",\"USING\",\"UTC_DATE\",\"UTC_TIME\",\"UTC_TIMESTAMP\",\"VALUES\",\"WHEN\",\"WHERE\",\"WHILE\",\"WINDOW\",\"WITH\",\"WRITE\",\"XOR\",\"YEAR_MONTH\",\"ZEROFILL\"],eS=[\"BIGINT\",\"BINARY\",\"BIT\",\"BLOB\",\"CHAR BYTE\",\"CHAR\",\"CHARACTER\",\"DATETIME\",\"DEC\",\"DECIMAL\",\"DOUBLE PRECISION\",\"DOUBLE\",\"ENUM\",\"FIXED\",\"FLOAT\",\"FLOAT4\",\"FLOAT8\",\"INT\",\"INT1\",\"INT2\",\"INT3\",\"INT4\",\"INT8\",\"INTEGER\",\"LONG\",\"LONGBLOB\",\"LONGTEXT\",\"MEDIUMBLOB\",\"MEDIUMINT\",\"MEDIUMTEXT\",\"MIDDLEINT\",\"NATIONAL CHAR\",\"NATIONAL VARCHAR\",\"NUMERIC\",\"PRECISION\",\"REAL\",\"SMALLINT\",\"TEXT\",\"TIMESTAMP\",\"TINYBLOB\",\"TINYINT\",\"TINYTEXT\",\"VARBINARY\",\"VARCHAR\",\"VARCHARACTER\",\"VARYING\",\"YEAR\"],ES=[\"ADDDATE\",\"ADD_MONTHS\",\"BIT_AND\",\"BIT_OR\",\"BIT_XOR\",\"CAST\",\"COUNT\",\"CUME_DIST\",\"CURDATE\",\"CURTIME\",\"DATE_ADD\",\"DATE_SUB\",\"DATE_FORMAT\",\"DECODE\",\"DENSE_RANK\",\"EXTRACT\",\"FIRST_VALUE\",\"GROUP_CONCAT\",\"JSON_ARRAYAGG\",\"JSON_OBJECTAGG\",\"LAG\",\"LEAD\",\"MAX\",\"MEDIAN\",\"MID\",\"MIN\",\"NOW\",\"NTH_VALUE\",\"NTILE\",\"POSITION\",\"PERCENT_RANK\",\"PERCENTILE_CONT\",\"PERCENTILE_DISC\",\"RANK\",\"ROW_NUMBER\",\"SESSION_USER\",\"STD\",\"STDDEV\",\"STDDEV_POP\",\"STDDEV_SAMP\",\"SUBDATE\",\"SUBSTR\",\"SUBSTRING\",\"SUM\",\"SYSTEM_USER\",\"TRIM\",\"TRIM_ORACLE\",\"VARIANCE\",\"VAR_POP\",\"VAR_SAMP\",\"ABS\",\"ACOS\",\"ADDTIME\",\"AES_DECRYPT\",\"AES_ENCRYPT\",\"ASIN\",\"ATAN\",\"ATAN2\",\"BENCHMARK\",\"BIN\",\"BINLOG_GTID_POS\",\"BIT_COUNT\",\"BIT_LENGTH\",\"CEIL\",\"CEILING\",\"CHARACTER_LENGTH\",\"CHAR_LENGTH\",\"CHR\",\"COERCIBILITY\",\"COLUMN_CHECK\",\"COLUMN_EXISTS\",\"COLUMN_LIST\",\"COLUMN_JSON\",\"COMPRESS\",\"CONCAT\",\"CONCAT_OPERATOR_ORACLE\",\"CONCAT_WS\",\"CONNECTION_ID\",\"CONV\",\"CONVERT_TZ\",\"COS\",\"COT\",\"CRC32\",\"DATEDIFF\",\"DAYNAME\",\"DAYOFMONTH\",\"DAYOFWEEK\",\"DAYOFYEAR\",\"DEGREES\",\"DECODE_HISTOGRAM\",\"DECODE_ORACLE\",\"DES_DECRYPT\",\"DES_ENCRYPT\",\"ELT\",\"ENCODE\",\"ENCRYPT\",\"EXP\",\"EXPORT_SET\",\"EXTRACTVALUE\",\"FIELD\",\"FIND_IN_SET\",\"FLOOR\",\"FORMAT\",\"FOUND_ROWS\",\"FROM_BASE64\",\"FROM_DAYS\",\"FROM_UNIXTIME\",\"GET_LOCK\",\"GREATEST\",\"HEX\",\"IFNULL\",\"INSTR\",\"ISNULL\",\"IS_FREE_LOCK\",\"IS_USED_LOCK\",\"JSON_ARRAY\",\"JSON_ARRAY_APPEND\",\"JSON_ARRAY_INSERT\",\"JSON_COMPACT\",\"JSON_CONTAINS\",\"JSON_CONTAINS_PATH\",\"JSON_DEPTH\",\"JSON_DETAILED\",\"JSON_EXISTS\",\"JSON_EXTRACT\",\"JSON_INSERT\",\"JSON_KEYS\",\"JSON_LENGTH\",\"JSON_LOOSE\",\"JSON_MERGE\",\"JSON_MERGE_PATCH\",\"JSON_MERGE_PRESERVE\",\"JSON_QUERY\",\"JSON_QUOTE\",\"JSON_OBJECT\",\"JSON_REMOVE\",\"JSON_REPLACE\",\"JSON_SET\",\"JSON_SEARCH\",\"JSON_TYPE\",\"JSON_UNQUOTE\",\"JSON_VALID\",\"JSON_VALUE\",\"LAST_DAY\",\"LAST_INSERT_ID\",\"LCASE\",\"LEAST\",\"LENGTH\",\"LENGTHB\",\"LN\",\"LOAD_FILE\",\"LOCATE\",\"LOG\",\"LOG10\",\"LOG2\",\"LOWER\",\"LPAD\",\"LPAD_ORACLE\",\"LTRIM\",\"LTRIM_ORACLE\",\"MAKEDATE\",\"MAKETIME\",\"MAKE_SET\",\"MASTER_GTID_WAIT\",\"MASTER_POS_WAIT\",\"MD5\",\"MONTHNAME\",\"NAME_CONST\",\"NVL\",\"NVL2\",\"OCT\",\"OCTET_LENGTH\",\"ORD\",\"PERIOD_ADD\",\"PERIOD_DIFF\",\"PI\",\"POW\",\"POWER\",\"QUOTE\",\"REGEXP_INSTR\",\"REGEXP_REPLACE\",\"REGEXP_SUBSTR\",\"RADIANS\",\"RAND\",\"RELEASE_ALL_LOCKS\",\"RELEASE_LOCK\",\"REPLACE_ORACLE\",\"REVERSE\",\"ROUND\",\"RPAD\",\"RPAD_ORACLE\",\"RTRIM\",\"RTRIM_ORACLE\",\"SEC_TO_TIME\",\"SHA\",\"SHA1\",\"SHA2\",\"SIGN\",\"SIN\",\"SLEEP\",\"SOUNDEX\",\"SPACE\",\"SQRT\",\"STRCMP\",\"STR_TO_DATE\",\"SUBSTR_ORACLE\",\"SUBSTRING_INDEX\",\"SUBTIME\",\"SYS_GUID\",\"TAN\",\"TIMEDIFF\",\"TIME_FORMAT\",\"TIME_TO_SEC\",\"TO_BASE64\",\"TO_CHAR\",\"TO_DAYS\",\"TO_SECONDS\",\"UCASE\",\"UNCOMPRESS\",\"UNCOMPRESSED_LENGTH\",\"UNHEX\",\"UNIX_TIMESTAMP\",\"UPDATEXML\",\"UPPER\",\"UUID\",\"UUID_SHORT\",\"VERSION\",\"WEEKDAY\",\"WEEKOFYEAR\",\"WSREP_LAST_WRITTEN_GTID\",\"WSREP_LAST_SEEN_GTID\",\"WSREP_SYNC_WAIT_UPTO_GTID\",\"YEARWEEK\",\"COALESCE\",\"NULLIF\"],tS=v([\"SELECT [ALL | DISTINCT | DISTINCTROW]\"]),TS=v([\"WITH [RECURSIVE]\",\"FROM\",\"WHERE\",\"GROUP BY\",\"HAVING\",\"PARTITION BY\",\"ORDER BY\",\"LIMIT\",\"OFFSET\",\"FETCH {FIRST | NEXT}\",\"INSERT [LOW_PRIORITY | DELAYED | HIGH_PRIORITY] [IGNORE] [INTO]\",\"REPLACE [LOW_PRIORITY | DELAYED] [INTO]\",\"VALUES\",\"ON DUPLICATE KEY UPDATE\",\"SET\",\"RETURNING\"]),Ar=v([\"CREATE [OR REPLACE] [TEMPORARY] TABLE [IF NOT EXISTS]\"]),Jt=v([\"CREATE [OR REPLACE] [SQL SECURITY DEFINER | SQL SECURITY INVOKER] VIEW [IF NOT EXISTS]\",\"UPDATE [LOW_PRIORITY] [IGNORE]\",\"DELETE [LOW_PRIORITY] [QUICK] [IGNORE] FROM\",\"DROP [TEMPORARY] TABLE [IF EXISTS]\",\"ALTER [ONLINE] [IGNORE] TABLE [IF EXISTS]\",\"ADD [COLUMN] [IF NOT EXISTS]\",\"{CHANGE | MODIFY} [COLUMN] [IF EXISTS]\",\"DROP [COLUMN] [IF EXISTS]\",\"RENAME [TO]\",\"RENAME COLUMN\",\"ALTER [COLUMN]\",\"{SET | DROP} DEFAULT\",\"SET {VISIBLE | INVISIBLE}\",\"TRUNCATE [TABLE]\",\"ALTER DATABASE\",\"ALTER DATABASE COMMENT\",\"ALTER EVENT\",\"ALTER FUNCTION\",\"ALTER PROCEDURE\",\"ALTER SCHEMA\",\"ALTER SCHEMA COMMENT\",\"ALTER SEQUENCE\",\"ALTER SERVER\",\"ALTER USER\",\"ALTER VIEW\",\"ANALYZE\",\"ANALYZE TABLE\",\"BACKUP LOCK\",\"BACKUP STAGE\",\"BACKUP UNLOCK\",\"BEGIN\",\"BINLOG\",\"CACHE INDEX\",\"CALL\",\"CHANGE MASTER TO\",\"CHECK TABLE\",\"CHECK VIEW\",\"CHECKSUM TABLE\",\"COMMIT\",\"CREATE AGGREGATE FUNCTION\",\"CREATE DATABASE\",\"CREATE EVENT\",\"CREATE FUNCTION\",\"CREATE INDEX\",\"CREATE PROCEDURE\",\"CREATE ROLE\",\"CREATE SEQUENCE\",\"CREATE SERVER\",\"CREATE SPATIAL INDEX\",\"CREATE TRIGGER\",\"CREATE UNIQUE INDEX\",\"CREATE USER\",\"DEALLOCATE PREPARE\",\"DESCRIBE\",\"DROP DATABASE\",\"DROP EVENT\",\"DROP FUNCTION\",\"DROP INDEX\",\"DROP PREPARE\",\"DROP PROCEDURE\",\"DROP ROLE\",\"DROP SEQUENCE\",\"DROP SERVER\",\"DROP TRIGGER\",\"DROP USER\",\"DROP VIEW\",\"EXECUTE\",\"EXPLAIN\",\"FLUSH\",\"GET DIAGNOSTICS\",\"GET DIAGNOSTICS CONDITION\",\"GRANT\",\"HANDLER\",\"HELP\",\"INSTALL PLUGIN\",\"INSTALL SONAME\",\"KILL\",\"LOAD DATA INFILE\",\"LOAD INDEX INTO CACHE\",\"LOAD XML INFILE\",\"LOCK TABLE\",\"OPTIMIZE TABLE\",\"PREPARE\",\"PURGE BINARY LOGS\",\"PURGE MASTER LOGS\",\"RELEASE SAVEPOINT\",\"RENAME TABLE\",\"RENAME USER\",\"REPAIR TABLE\",\"REPAIR VIEW\",\"RESET MASTER\",\"RESET QUERY CACHE\",\"RESET REPLICA\",\"RESET SLAVE\",\"RESIGNAL\",\"REVOKE\",\"ROLLBACK\",\"SAVEPOINT\",\"SET CHARACTER SET\",\"SET DEFAULT ROLE\",\"SET GLOBAL TRANSACTION\",\"SET NAMES\",\"SET PASSWORD\",\"SET ROLE\",\"SET STATEMENT\",\"SET TRANSACTION\",\"SHOW\",\"SHOW ALL REPLICAS STATUS\",\"SHOW ALL SLAVES STATUS\",\"SHOW AUTHORS\",\"SHOW BINARY LOGS\",\"SHOW BINLOG EVENTS\",\"SHOW BINLOG STATUS\",\"SHOW CHARACTER SET\",\"SHOW CLIENT_STATISTICS\",\"SHOW COLLATION\",\"SHOW COLUMNS\",\"SHOW CONTRIBUTORS\",\"SHOW CREATE DATABASE\",\"SHOW CREATE EVENT\",\"SHOW CREATE FUNCTION\",\"SHOW CREATE PACKAGE\",\"SHOW CREATE PACKAGE BODY\",\"SHOW CREATE PROCEDURE\",\"SHOW CREATE SEQUENCE\",\"SHOW CREATE TABLE\",\"SHOW CREATE TRIGGER\",\"SHOW CREATE USER\",\"SHOW CREATE VIEW\",\"SHOW DATABASES\",\"SHOW ENGINE\",\"SHOW ENGINE INNODB STATUS\",\"SHOW ENGINES\",\"SHOW ERRORS\",\"SHOW EVENTS\",\"SHOW EXPLAIN\",\"SHOW FUNCTION CODE\",\"SHOW FUNCTION STATUS\",\"SHOW GRANTS\",\"SHOW INDEX\",\"SHOW INDEXES\",\"SHOW INDEX_STATISTICS\",\"SHOW KEYS\",\"SHOW LOCALES\",\"SHOW MASTER LOGS\",\"SHOW MASTER STATUS\",\"SHOW OPEN TABLES\",\"SHOW PACKAGE BODY CODE\",\"SHOW PACKAGE BODY STATUS\",\"SHOW PACKAGE STATUS\",\"SHOW PLUGINS\",\"SHOW PLUGINS SONAME\",\"SHOW PRIVILEGES\",\"SHOW PROCEDURE CODE\",\"SHOW PROCEDURE STATUS\",\"SHOW PROCESSLIST\",\"SHOW PROFILE\",\"SHOW PROFILES\",\"SHOW QUERY_RESPONSE_TIME\",\"SHOW RELAYLOG EVENTS\",\"SHOW REPLICA\",\"SHOW REPLICA HOSTS\",\"SHOW REPLICA STATUS\",\"SHOW SCHEMAS\",\"SHOW SLAVE\",\"SHOW SLAVE HOSTS\",\"SHOW SLAVE STATUS\",\"SHOW STATUS\",\"SHOW STORAGE ENGINES\",\"SHOW TABLE STATUS\",\"SHOW TABLES\",\"SHOW TRIGGERS\",\"SHOW USER_STATISTICS\",\"SHOW VARIABLES\",\"SHOW WARNINGS\",\"SHOW WSREP_MEMBERSHIP\",\"SHOW WSREP_STATUS\",\"SHUTDOWN\",\"SIGNAL\",\"START ALL REPLICAS\",\"START ALL SLAVES\",\"START REPLICA\",\"START SLAVE\",\"START TRANSACTION\",\"STOP ALL REPLICAS\",\"STOP ALL SLAVES\",\"STOP REPLICA\",\"STOP SLAVE\",\"UNINSTALL PLUGIN\",\"UNINSTALL SONAME\",\"UNLOCK TABLE\",\"USE\",\"XA BEGIN\",\"XA COMMIT\",\"XA END\",\"XA PREPARE\",\"XA RECOVER\",\"XA ROLLBACK\",\"XA START\"]),rS=v([\"UNION [ALL | DISTINCT]\",\"EXCEPT [ALL | DISTINCT]\",\"INTERSECT [ALL | DISTINCT]\",\"MINUS [ALL | DISTINCT]\"]),RS=v([\"JOIN\",\"{LEFT | RIGHT} [OUTER] JOIN\",\"{INNER | CROSS} JOIN\",\"NATURAL JOIN\",\"NATURAL {LEFT | RIGHT} [OUTER] JOIN\",\"STRAIGHT_JOIN\"]),nS=v([\"ON {UPDATE | DELETE} [SET NULL | SET DEFAULT]\",\"CHARACTER SET\",\"{ROWS | RANGE} BETWEEN\",\"IDENTIFIED BY\"]),AS={name:\"mariadb\",tokenizerOptions:{reservedSelect:tS,reservedClauses:[...TS,...Ar,...Jt],reservedSetOperations:rS,reservedJoins:RS,reservedPhrases:nS,supportsXor:!0,reservedKeywords:zs,reservedDataTypes:eS,reservedFunctionNames:ES,stringTypes:['\"\"-qq-bs',\"''-qq-bs\",{quote:\"''-raw\",prefixes:[\"B\",\"X\"],requirePrefix:!0}],identTypes:[\"``\"],identChars:{first:\"$\",rest:\"$\",allowFirstCharNumber:!0},variableTypes:[{regex:\"@@?[A-Za-z0-9_.$]+\"},{quote:'\"\"-qq-bs',prefixes:[\"@\"],requirePrefix:!0},{quote:\"''-qq-bs\",prefixes:[\"@\"],requirePrefix:!0},{quote:\"``\",prefixes:[\"@\"],requirePrefix:!0}],paramTypes:{positional:!0},lineCommentTypes:[\"--\",\"#\"],operators:[\"%\",\":=\",\"&\",\"|\",\"^\",\"~\",\"<<\",\">>\",\"<=>\",\"&&\",\"||\",\"!\",\"*.*\"],postProcess:yt},formatOptions:{onelineClauses:[...Ar,...Jt],tabularOnelineClauses:Jt}},sS=[\"ACCESSIBLE\",\"ADD\",\"ALL\",\"ALTER\",\"ANALYZE\",\"AND\",\"AS\",\"ASC\",\"ASENSITIVE\",\"BEFORE\",\"BETWEEN\",\"BOTH\",\"BY\",\"CALL\",\"CASCADE\",\"CASE\",\"CHANGE\",\"CHECK\",\"COLLATE\",\"COLUMN\",\"CONDITION\",\"CONSTRAINT\",\"CONTINUE\",\"CONVERT\",\"CREATE\",\"CROSS\",\"CUBE\",\"CUME_DIST\",\"CURRENT_DATE\",\"CURRENT_TIME\",\"CURRENT_TIMESTAMP\",\"CURRENT_USER\",\"CURSOR\",\"DATABASE\",\"DATABASES\",\"DAY_HOUR\",\"DAY_MICROSECOND\",\"DAY_MINUTE\",\"DAY_SECOND\",\"DECLARE\",\"DEFAULT\",\"DELAYED\",\"DELETE\",\"DENSE_RANK\",\"DESC\",\"DESCRIBE\",\"DETERMINISTIC\",\"DISTINCT\",\"DISTINCTROW\",\"DIV\",\"DROP\",\"DUAL\",\"EACH\",\"ELSE\",\"ELSEIF\",\"EMPTY\",\"ENCLOSED\",\"ESCAPED\",\"EXCEPT\",\"EXISTS\",\"EXIT\",\"EXPLAIN\",\"FALSE\",\"FETCH\",\"FIRST_VALUE\",\"FOR\",\"FORCE\",\"FOREIGN\",\"FROM\",\"FULLTEXT\",\"FUNCTION\",\"GENERATED\",\"GET\",\"GRANT\",\"GROUP\",\"GROUPING\",\"GROUPS\",\"HAVING\",\"HIGH_PRIORITY\",\"HOUR_MICROSECOND\",\"HOUR_MINUTE\",\"HOUR_SECOND\",\"IF\",\"IGNORE\",\"IN\",\"INDEX\",\"INFILE\",\"INNER\",\"INOUT\",\"INSENSITIVE\",\"INSERT\",\"IN\",\"INTERSECT\",\"INTERVAL\",\"INTO\",\"IO_AFTER_GTIDS\",\"IO_BEFORE_GTIDS\",\"IS\",\"ITERATE\",\"JOIN\",\"JSON_TABLE\",\"KEY\",\"KEYS\",\"KILL\",\"LAG\",\"LAST_VALUE\",\"LATERAL\",\"LEAD\",\"LEADING\",\"LEAVE\",\"LEFT\",\"LIKE\",\"LIMIT\",\"LINEAR\",\"LINES\",\"LOAD\",\"LOCALTIME\",\"LOCALTIMESTAMP\",\"LOCK\",\"LONG\",\"LOOP\",\"LOW_PRIORITY\",\"MASTER_BIND\",\"MASTER_SSL_VERIFY_SERVER_CERT\",\"MATCH\",\"MAXVALUE\",\"MINUTE_MICROSECOND\",\"MINUTE_SECOND\",\"MOD\",\"MODIFIES\",\"NATURAL\",\"NOT\",\"NO_WRITE_TO_BINLOG\",\"NTH_VALUE\",\"NTILE\",\"NULL\",\"OF\",\"ON\",\"OPTIMIZE\",\"OPTIMIZER_COSTS\",\"OPTION\",\"OPTIONALLY\",\"OR\",\"ORDER\",\"OUT\",\"OUTER\",\"OUTFILE\",\"OVER\",\"PARTITION\",\"PERCENT_RANK\",\"PRIMARY\",\"PROCEDURE\",\"PURGE\",\"RANGE\",\"RANK\",\"READ\",\"READS\",\"READ_WRITE\",\"RECURSIVE\",\"REFERENCES\",\"REGEXP\",\"RELEASE\",\"RENAME\",\"REPEAT\",\"REPLACE\",\"REQUIRE\",\"RESIGNAL\",\"RESTRICT\",\"RETURN\",\"REVOKE\",\"RIGHT\",\"RLIKE\",\"ROW\",\"ROWS\",\"ROW_NUMBER\",\"SCHEMA\",\"SCHEMAS\",\"SECOND_MICROSECOND\",\"SELECT\",\"SENSITIVE\",\"SEPARATOR\",\"SET\",\"SHOW\",\"SIGNAL\",\"SPATIAL\",\"SPECIFIC\",\"SQL\",\"SQLEXCEPTION\",\"SQLSTATE\",\"SQLWARNING\",\"SQL_BIG_RESULT\",\"SQL_CALC_FOUND_ROWS\",\"SQL_SMALL_RESULT\",\"SSL\",\"STARTING\",\"STORED\",\"STRAIGHT_JOIN\",\"SYSTEM\",\"TABLE\",\"TERMINATED\",\"THEN\",\"TO\",\"TRAILING\",\"TRIGGER\",\"TRUE\",\"UNDO\",\"UNION\",\"UNIQUE\",\"UNLOCK\",\"UNSIGNED\",\"UPDATE\",\"USAGE\",\"USE\",\"USING\",\"UTC_DATE\",\"UTC_TIME\",\"UTC_TIMESTAMP\",\"VALUES\",\"VIRTUAL\",\"WHEN\",\"WHERE\",\"WHILE\",\"WINDOW\",\"WITH\",\"WRITE\",\"XOR\",\"YEAR_MONTH\",\"ZEROFILL\"],SS=[\"BIGINT\",\"BINARY\",\"BIT\",\"BLOB\",\"BOOL\",\"BOOLEAN\",\"CHAR\",\"CHARACTER\",\"DATE\",\"DATETIME\",\"DEC\",\"DECIMAL\",\"DOUBLE PRECISION\",\"DOUBLE\",\"ENUM\",\"FIXED\",\"FLOAT\",\"FLOAT4\",\"FLOAT8\",\"INT\",\"INT1\",\"INT2\",\"INT3\",\"INT4\",\"INT8\",\"INTEGER\",\"LONGBLOB\",\"LONGTEXT\",\"MEDIUMBLOB\",\"MEDIUMINT\",\"MEDIUMTEXT\",\"MIDDLEINT\",\"NATIONAL CHAR\",\"NATIONAL VARCHAR\",\"NUMERIC\",\"PRECISION\",\"REAL\",\"SMALLINT\",\"TEXT\",\"TIME\",\"TIMESTAMP\",\"TINYBLOB\",\"TINYINT\",\"TINYTEXT\",\"VARBINARY\",\"VARCHAR\",\"VARCHARACTER\",\"VARYING\",\"YEAR\"],oS=[\"ABS\",\"ACOS\",\"ADDDATE\",\"ADDTIME\",\"AES_DECRYPT\",\"AES_ENCRYPT\",\"ANY_VALUE\",\"ASCII\",\"ASIN\",\"ATAN\",\"ATAN2\",\"AVG\",\"BENCHMARK\",\"BIN\",\"BIN_TO_UUID\",\"BINARY\",\"BIT_AND\",\"BIT_COUNT\",\"BIT_LENGTH\",\"BIT_OR\",\"BIT_XOR\",\"CAN_ACCESS_COLUMN\",\"CAN_ACCESS_DATABASE\",\"CAN_ACCESS_TABLE\",\"CAN_ACCESS_USER\",\"CAN_ACCESS_VIEW\",\"CAST\",\"CEIL\",\"CEILING\",\"CHAR\",\"CHAR_LENGTH\",\"CHARACTER_LENGTH\",\"CHARSET\",\"COALESCE\",\"COERCIBILITY\",\"COLLATION\",\"COMPRESS\",\"CONCAT\",\"CONCAT_WS\",\"CONNECTION_ID\",\"CONV\",\"CONVERT\",\"CONVERT_TZ\",\"COS\",\"COT\",\"COUNT\",\"CRC32\",\"CUME_DIST\",\"CURDATE\",\"CURRENT_DATE\",\"CURRENT_ROLE\",\"CURRENT_TIME\",\"CURRENT_TIMESTAMP\",\"CURRENT_USER\",\"CURTIME\",\"DATABASE\",\"DATE\",\"DATE_ADD\",\"DATE_FORMAT\",\"DATE_SUB\",\"DATEDIFF\",\"DAY\",\"DAYNAME\",\"DAYOFMONTH\",\"DAYOFWEEK\",\"DAYOFYEAR\",\"DEFAULT\",\"DEGREES\",\"DENSE_RANK\",\"DIV\",\"ELT\",\"EXP\",\"EXPORT_SET\",\"EXTRACT\",\"EXTRACTVALUE\",\"FIELD\",\"FIND_IN_SET\",\"FIRST_VALUE\",\"FLOOR\",\"FORMAT\",\"FORMAT_BYTES\",\"FORMAT_PICO_TIME\",\"FOUND_ROWS\",\"FROM_BASE64\",\"FROM_DAYS\",\"FROM_UNIXTIME\",\"GEOMCOLLECTION\",\"GEOMETRYCOLLECTION\",\"GET_DD_COLUMN_PRIVILEGES\",\"GET_DD_CREATE_OPTIONS\",\"GET_DD_INDEX_SUB_PART_LENGTH\",\"GET_FORMAT\",\"GET_LOCK\",\"GREATEST\",\"GROUP_CONCAT\",\"GROUPING\",\"GTID_SUBSET\",\"GTID_SUBTRACT\",\"HEX\",\"HOUR\",\"ICU_VERSION\",\"IF\",\"IFNULL\",\"INET_ATON\",\"INET_NTOA\",\"INET6_ATON\",\"INET6_NTOA\",\"INSERT\",\"INSTR\",\"INTERNAL_AUTO_INCREMENT\",\"INTERNAL_AVG_ROW_LENGTH\",\"INTERNAL_CHECK_TIME\",\"INTERNAL_CHECKSUM\",\"INTERNAL_DATA_FREE\",\"INTERNAL_DATA_LENGTH\",\"INTERNAL_DD_CHAR_LENGTH\",\"INTERNAL_GET_COMMENT_OR_ERROR\",\"INTERNAL_GET_ENABLED_ROLE_JSON\",\"INTERNAL_GET_HOSTNAME\",\"INTERNAL_GET_USERNAME\",\"INTERNAL_GET_VIEW_WARNING_OR_ERROR\",\"INTERNAL_INDEX_COLUMN_CARDINALITY\",\"INTERNAL_INDEX_LENGTH\",\"INTERNAL_IS_ENABLED_ROLE\",\"INTERNAL_IS_MANDATORY_ROLE\",\"INTERNAL_KEYS_DISABLED\",\"INTERNAL_MAX_DATA_LENGTH\",\"INTERNAL_TABLE_ROWS\",\"INTERNAL_UPDATE_TIME\",\"INTERVAL\",\"IS\",\"IS_FREE_LOCK\",\"IS_IPV4\",\"IS_IPV4_COMPAT\",\"IS_IPV4_MAPPED\",\"IS_IPV6\",\"IS NOT\",\"IS NOT NULL\",\"IS NULL\",\"IS_USED_LOCK\",\"IS_UUID\",\"ISNULL\",\"JSON_ARRAY\",\"JSON_ARRAY_APPEND\",\"JSON_ARRAY_INSERT\",\"JSON_ARRAYAGG\",\"JSON_CONTAINS\",\"JSON_CONTAINS_PATH\",\"JSON_DEPTH\",\"JSON_EXTRACT\",\"JSON_INSERT\",\"JSON_KEYS\",\"JSON_LENGTH\",\"JSON_MERGE\",\"JSON_MERGE_PATCH\",\"JSON_MERGE_PRESERVE\",\"JSON_OBJECT\",\"JSON_OBJECTAGG\",\"JSON_OVERLAPS\",\"JSON_PRETTY\",\"JSON_QUOTE\",\"JSON_REMOVE\",\"JSON_REPLACE\",\"JSON_SCHEMA_VALID\",\"JSON_SCHEMA_VALIDATION_REPORT\",\"JSON_SEARCH\",\"JSON_SET\",\"JSON_STORAGE_FREE\",\"JSON_STORAGE_SIZE\",\"JSON_TABLE\",\"JSON_TYPE\",\"JSON_UNQUOTE\",\"JSON_VALID\",\"JSON_VALUE\",\"LAG\",\"LAST_DAY\",\"LAST_INSERT_ID\",\"LAST_VALUE\",\"LCASE\",\"LEAD\",\"LEAST\",\"LEFT\",\"LENGTH\",\"LIKE\",\"LINESTRING\",\"LN\",\"LOAD_FILE\",\"LOCALTIME\",\"LOCALTIMESTAMP\",\"LOCATE\",\"LOG\",\"LOG10\",\"LOG2\",\"LOWER\",\"LPAD\",\"LTRIM\",\"MAKE_SET\",\"MAKEDATE\",\"MAKETIME\",\"MASTER_POS_WAIT\",\"MATCH\",\"MAX\",\"MBRCONTAINS\",\"MBRCOVEREDBY\",\"MBRCOVERS\",\"MBRDISJOINT\",\"MBREQUALS\",\"MBRINTERSECTS\",\"MBROVERLAPS\",\"MBRTOUCHES\",\"MBRWITHIN\",\"MD5\",\"MEMBER OF\",\"MICROSECOND\",\"MID\",\"MIN\",\"MINUTE\",\"MOD\",\"MONTH\",\"MONTHNAME\",\"MULTILINESTRING\",\"MULTIPOINT\",\"MULTIPOLYGON\",\"NAME_CONST\",\"NOT\",\"NOT IN\",\"NOT LIKE\",\"NOT REGEXP\",\"NOW\",\"NTH_VALUE\",\"NTILE\",\"NULLIF\",\"OCT\",\"OCTET_LENGTH\",\"ORD\",\"PERCENT_RANK\",\"PERIOD_ADD\",\"PERIOD_DIFF\",\"PI\",\"POINT\",\"POLYGON\",\"POSITION\",\"POW\",\"POWER\",\"PS_CURRENT_THREAD_ID\",\"PS_THREAD_ID\",\"QUARTER\",\"QUOTE\",\"RADIANS\",\"RAND\",\"RANDOM_BYTES\",\"RANK\",\"REGEXP\",\"REGEXP_INSTR\",\"REGEXP_LIKE\",\"REGEXP_REPLACE\",\"REGEXP_SUBSTR\",\"RELEASE_ALL_LOCKS\",\"RELEASE_LOCK\",\"REPEAT\",\"REPLACE\",\"REVERSE\",\"RIGHT\",\"RLIKE\",\"ROLES_GRAPHML\",\"ROUND\",\"ROW_COUNT\",\"ROW_NUMBER\",\"RPAD\",\"RTRIM\",\"SCHEMA\",\"SEC_TO_TIME\",\"SECOND\",\"SESSION_USER\",\"SHA1\",\"SHA2\",\"SIGN\",\"SIN\",\"SLEEP\",\"SOUNDEX\",\"SOUNDS LIKE\",\"SOURCE_POS_WAIT\",\"SPACE\",\"SQRT\",\"ST_AREA\",\"ST_ASBINARY\",\"ST_ASGEOJSON\",\"ST_ASTEXT\",\"ST_BUFFER\",\"ST_BUFFER_STRATEGY\",\"ST_CENTROID\",\"ST_COLLECT\",\"ST_CONTAINS\",\"ST_CONVEXHULL\",\"ST_CROSSES\",\"ST_DIFFERENCE\",\"ST_DIMENSION\",\"ST_DISJOINT\",\"ST_DISTANCE\",\"ST_DISTANCE_SPHERE\",\"ST_ENDPOINT\",\"ST_ENVELOPE\",\"ST_EQUALS\",\"ST_EXTERIORRING\",\"ST_FRECHETDISTANCE\",\"ST_GEOHASH\",\"ST_GEOMCOLLFROMTEXT\",\"ST_GEOMCOLLFROMWKB\",\"ST_GEOMETRYN\",\"ST_GEOMETRYTYPE\",\"ST_GEOMFROMGEOJSON\",\"ST_GEOMFROMTEXT\",\"ST_GEOMFROMWKB\",\"ST_HAUSDORFFDISTANCE\",\"ST_INTERIORRINGN\",\"ST_INTERSECTION\",\"ST_INTERSECTS\",\"ST_ISCLOSED\",\"ST_ISEMPTY\",\"ST_ISSIMPLE\",\"ST_ISVALID\",\"ST_LATFROMGEOHASH\",\"ST_LATITUDE\",\"ST_LENGTH\",\"ST_LINEFROMTEXT\",\"ST_LINEFROMWKB\",\"ST_LINEINTERPOLATEPOINT\",\"ST_LINEINTERPOLATEPOINTS\",\"ST_LONGFROMGEOHASH\",\"ST_LONGITUDE\",\"ST_MAKEENVELOPE\",\"ST_MLINEFROMTEXT\",\"ST_MLINEFROMWKB\",\"ST_MPOINTFROMTEXT\",\"ST_MPOINTFROMWKB\",\"ST_MPOLYFROMTEXT\",\"ST_MPOLYFROMWKB\",\"ST_NUMGEOMETRIES\",\"ST_NUMINTERIORRING\",\"ST_NUMPOINTS\",\"ST_OVERLAPS\",\"ST_POINTATDISTANCE\",\"ST_POINTFROMGEOHASH\",\"ST_POINTFROMTEXT\",\"ST_POINTFROMWKB\",\"ST_POINTN\",\"ST_POLYFROMTEXT\",\"ST_POLYFROMWKB\",\"ST_SIMPLIFY\",\"ST_SRID\",\"ST_STARTPOINT\",\"ST_SWAPXY\",\"ST_SYMDIFFERENCE\",\"ST_TOUCHES\",\"ST_TRANSFORM\",\"ST_UNION\",\"ST_VALIDATE\",\"ST_WITHIN\",\"ST_X\",\"ST_Y\",\"STATEMENT_DIGEST\",\"STATEMENT_DIGEST_TEXT\",\"STD\",\"STDDEV\",\"STDDEV_POP\",\"STDDEV_SAMP\",\"STR_TO_DATE\",\"STRCMP\",\"SUBDATE\",\"SUBSTR\",\"SUBSTRING\",\"SUBSTRING_INDEX\",\"SUBTIME\",\"SUM\",\"SYSDATE\",\"SYSTEM_USER\",\"TAN\",\"TIME\",\"TIME_FORMAT\",\"TIME_TO_SEC\",\"TIMEDIFF\",\"TIMESTAMP\",\"TIMESTAMPADD\",\"TIMESTAMPDIFF\",\"TO_BASE64\",\"TO_DAYS\",\"TO_SECONDS\",\"TRIM\",\"TRUNCATE\",\"UCASE\",\"UNCOMPRESS\",\"UNCOMPRESSED_LENGTH\",\"UNHEX\",\"UNIX_TIMESTAMP\",\"UPDATEXML\",\"UPPER\",\"UTC_DATE\",\"UTC_TIME\",\"UTC_TIMESTAMP\",\"UUID\",\"UUID_SHORT\",\"UUID_TO_BIN\",\"VALIDATE_PASSWORD_STRENGTH\",\"VALUES\",\"VAR_POP\",\"VAR_SAMP\",\"VARIANCE\",\"VERSION\",\"WAIT_FOR_EXECUTED_GTID_SET\",\"WAIT_UNTIL_SQL_THREAD_AFTER_GTIDS\",\"WEEK\",\"WEEKDAY\",\"WEEKOFYEAR\",\"WEIGHT_STRING\",\"YEAR\",\"YEARWEEK\"],OS=v([\"SELECT [ALL | DISTINCT | DISTINCTROW]\"]),iS=v([\"WITH [RECURSIVE]\",\"FROM\",\"WHERE\",\"GROUP BY\",\"HAVING\",\"WINDOW\",\"PARTITION BY\",\"ORDER BY\",\"LIMIT\",\"OFFSET\",\"INSERT [LOW_PRIORITY | DELAYED | HIGH_PRIORITY] [IGNORE] [INTO]\",\"REPLACE [LOW_PRIORITY | DELAYED] [INTO]\",\"VALUES\",\"ON DUPLICATE KEY UPDATE\",\"SET\"]),sr=v([\"CREATE [TEMPORARY] TABLE [IF NOT EXISTS]\"]),qt=v([\"CREATE [OR REPLACE] [SQL SECURITY DEFINER | SQL SECURITY INVOKER] VIEW [IF NOT EXISTS]\",\"UPDATE [LOW_PRIORITY] [IGNORE]\",\"DELETE [LOW_PRIORITY] [QUICK] [IGNORE] FROM\",\"DROP [TEMPORARY] TABLE [IF EXISTS]\",\"ALTER TABLE\",\"ADD [COLUMN]\",\"{CHANGE | MODIFY} [COLUMN]\",\"DROP [COLUMN]\",\"RENAME [TO | AS]\",\"RENAME COLUMN\",\"ALTER [COLUMN]\",\"{SET | DROP} DEFAULT\",\"TRUNCATE [TABLE]\",\"ALTER DATABASE\",\"ALTER EVENT\",\"ALTER FUNCTION\",\"ALTER INSTANCE\",\"ALTER LOGFILE GROUP\",\"ALTER PROCEDURE\",\"ALTER RESOURCE GROUP\",\"ALTER SERVER\",\"ALTER TABLESPACE\",\"ALTER USER\",\"ALTER VIEW\",\"ANALYZE TABLE\",\"BINLOG\",\"CACHE INDEX\",\"CALL\",\"CHANGE MASTER TO\",\"CHANGE REPLICATION FILTER\",\"CHANGE REPLICATION SOURCE TO\",\"CHECK TABLE\",\"CHECKSUM TABLE\",\"CLONE\",\"COMMIT\",\"CREATE DATABASE\",\"CREATE EVENT\",\"CREATE FUNCTION\",\"CREATE FUNCTION\",\"CREATE INDEX\",\"CREATE LOGFILE GROUP\",\"CREATE PROCEDURE\",\"CREATE RESOURCE GROUP\",\"CREATE ROLE\",\"CREATE SERVER\",\"CREATE SPATIAL REFERENCE SYSTEM\",\"CREATE TABLESPACE\",\"CREATE TRIGGER\",\"CREATE USER\",\"DEALLOCATE PREPARE\",\"DESCRIBE\",\"DROP DATABASE\",\"DROP EVENT\",\"DROP FUNCTION\",\"DROP FUNCTION\",\"DROP INDEX\",\"DROP LOGFILE GROUP\",\"DROP PROCEDURE\",\"DROP RESOURCE GROUP\",\"DROP ROLE\",\"DROP SERVER\",\"DROP SPATIAL REFERENCE SYSTEM\",\"DROP TABLESPACE\",\"DROP TRIGGER\",\"DROP USER\",\"DROP VIEW\",\"EXECUTE\",\"EXPLAIN\",\"FLUSH\",\"GRANT\",\"HANDLER\",\"HELP\",\"IMPORT TABLE\",\"INSTALL COMPONENT\",\"INSTALL PLUGIN\",\"KILL\",\"LOAD DATA\",\"LOAD INDEX INTO CACHE\",\"LOAD XML\",\"LOCK INSTANCE FOR BACKUP\",\"LOCK TABLES\",\"MASTER_POS_WAIT\",\"OPTIMIZE TABLE\",\"PREPARE\",\"PURGE BINARY LOGS\",\"RELEASE SAVEPOINT\",\"RENAME TABLE\",\"RENAME USER\",\"REPAIR TABLE\",\"RESET\",\"RESET MASTER\",\"RESET PERSIST\",\"RESET REPLICA\",\"RESET SLAVE\",\"RESTART\",\"REVOKE\",\"ROLLBACK\",\"ROLLBACK TO SAVEPOINT\",\"SAVEPOINT\",\"SET CHARACTER SET\",\"SET DEFAULT ROLE\",\"SET NAMES\",\"SET PASSWORD\",\"SET RESOURCE GROUP\",\"SET ROLE\",\"SET TRANSACTION\",\"SHOW\",\"SHOW BINARY LOGS\",\"SHOW BINLOG EVENTS\",\"SHOW CHARACTER SET\",\"SHOW COLLATION\",\"SHOW COLUMNS\",\"SHOW CREATE DATABASE\",\"SHOW CREATE EVENT\",\"SHOW CREATE FUNCTION\",\"SHOW CREATE PROCEDURE\",\"SHOW CREATE TABLE\",\"SHOW CREATE TRIGGER\",\"SHOW CREATE USER\",\"SHOW CREATE VIEW\",\"SHOW DATABASES\",\"SHOW ENGINE\",\"SHOW ENGINES\",\"SHOW ERRORS\",\"SHOW EVENTS\",\"SHOW FUNCTION CODE\",\"SHOW FUNCTION STATUS\",\"SHOW GRANTS\",\"SHOW INDEX\",\"SHOW MASTER STATUS\",\"SHOW OPEN TABLES\",\"SHOW PLUGINS\",\"SHOW PRIVILEGES\",\"SHOW PROCEDURE CODE\",\"SHOW PROCEDURE STATUS\",\"SHOW PROCESSLIST\",\"SHOW PROFILE\",\"SHOW PROFILES\",\"SHOW RELAYLOG EVENTS\",\"SHOW REPLICA STATUS\",\"SHOW REPLICAS\",\"SHOW SLAVE\",\"SHOW SLAVE HOSTS\",\"SHOW STATUS\",\"SHOW TABLE STATUS\",\"SHOW TABLES\",\"SHOW TRIGGERS\",\"SHOW VARIABLES\",\"SHOW WARNINGS\",\"SHUTDOWN\",\"SOURCE_POS_WAIT\",\"START GROUP_REPLICATION\",\"START REPLICA\",\"START SLAVE\",\"START TRANSACTION\",\"STOP GROUP_REPLICATION\",\"STOP REPLICA\",\"STOP SLAVE\",\"TABLE\",\"UNINSTALL COMPONENT\",\"UNINSTALL PLUGIN\",\"UNLOCK INSTANCE\",\"UNLOCK TABLES\",\"USE\",\"XA\",\"ITERATE\",\"LEAVE\",\"LOOP\",\"REPEAT\",\"RETURN\",\"WHILE\"]),aS=v([\"UNION [ALL | DISTINCT]\"]),IS=v([\"JOIN\",\"{LEFT | RIGHT} [OUTER] JOIN\",\"{INNER | CROSS} JOIN\",\"NATURAL [INNER] JOIN\",\"NATURAL {LEFT | RIGHT} [OUTER] JOIN\",\"STRAIGHT_JOIN\"]),NS=v([\"ON {UPDATE | DELETE} [SET NULL]\",\"CHARACTER SET\",\"{ROWS | RANGE} BETWEEN\",\"IDENTIFIED BY\"]),lS={name:\"mysql\",tokenizerOptions:{reservedSelect:OS,reservedClauses:[...iS,...sr,...qt],reservedSetOperations:aS,reservedJoins:IS,reservedPhrases:NS,supportsXor:!0,reservedKeywords:sS,reservedDataTypes:SS,reservedFunctionNames:oS,stringTypes:['\"\"-qq-bs',{quote:\"''-qq-bs\",prefixes:[\"N\"]},{quote:\"''-raw\",prefixes:[\"B\",\"X\"],requirePrefix:!0}],identTypes:[\"``\"],identChars:{first:\"$\",rest:\"$\",allowFirstCharNumber:!0},variableTypes:[{regex:\"@@?[A-Za-z0-9_.$]+\"},{quote:'\"\"-qq-bs',prefixes:[\"@\"],requirePrefix:!0},{quote:\"''-qq-bs\",prefixes:[\"@\"],requirePrefix:!0},{quote:\"``\",prefixes:[\"@\"],requirePrefix:!0}],paramTypes:{positional:!0},lineCommentTypes:[\"--\",\"#\"],operators:[\"%\",\":=\",\"&\",\"|\",\"^\",\"~\",\"<<\",\">>\",\"<=>\",\"->\",\"->>\",\"&&\",\"||\",\"!\",\"*.*\"],postProcess:yt},formatOptions:{onelineClauses:[...sr,...qt],tabularOnelineClauses:qt}},_S=[\"ADD\",\"ALL\",\"ALTER\",\"ANALYZE\",\"AND\",\"ARRAY\",\"AS\",\"ASC\",\"BETWEEN\",\"BOTH\",\"BY\",\"CALL\",\"CASCADE\",\"CASE\",\"CHANGE\",\"CHECK\",\"COLLATE\",\"COLUMN\",\"CONSTRAINT\",\"CONTINUE\",\"CONVERT\",\"CREATE\",\"CROSS\",\"CURRENT_DATE\",\"CURRENT_ROLE\",\"CURRENT_TIME\",\"CURRENT_TIMESTAMP\",\"CURRENT_USER\",\"CURSOR\",\"DATABASE\",\"DATABASES\",\"DAY_HOUR\",\"DAY_MICROSECOND\",\"DAY_MINUTE\",\"DAY_SECOND\",\"DEFAULT\",\"DELAYED\",\"DELETE\",\"DESC\",\"DESCRIBE\",\"DISTINCT\",\"DISTINCTROW\",\"DIV\",\"DOUBLE\",\"DROP\",\"DUAL\",\"ELSE\",\"ELSEIF\",\"ENCLOSED\",\"ESCAPED\",\"EXCEPT\",\"EXISTS\",\"EXIT\",\"EXPLAIN\",\"FALSE\",\"FETCH\",\"FOR\",\"FORCE\",\"FOREIGN\",\"FROM\",\"FULLTEXT\",\"GENERATED\",\"GRANT\",\"GROUP\",\"GROUPS\",\"HAVING\",\"HIGH_PRIORITY\",\"HOUR_MICROSECOND\",\"HOUR_MINUTE\",\"HOUR_SECOND\",\"IF\",\"IGNORE\",\"ILIKE\",\"IN\",\"INDEX\",\"INFILE\",\"INNER\",\"INOUT\",\"INSERT\",\"INTERSECT\",\"INTERVAL\",\"INTO\",\"IS\",\"ITERATE\",\"JOIN\",\"KEY\",\"KEYS\",\"KILL\",\"LEADING\",\"LEAVE\",\"LEFT\",\"LIKE\",\"LIMIT\",\"LINEAR\",\"LINES\",\"LOAD\",\"LOCALTIME\",\"LOCALTIMESTAMP\",\"LOCK\",\"LONG\",\"LOW_PRIORITY\",\"MATCH\",\"MAXVALUE\",\"MINUTE_MICROSECOND\",\"MINUTE_SECOND\",\"MOD\",\"NATURAL\",\"NOT\",\"NO_WRITE_TO_BINLOG\",\"NULL\",\"OF\",\"ON\",\"OPTIMIZE\",\"OPTION\",\"OPTIONALLY\",\"OR\",\"ORDER\",\"OUT\",\"OUTER\",\"OUTFILE\",\"OVER\",\"PARTITION\",\"PRIMARY\",\"PROCEDURE\",\"RANGE\",\"READ\",\"RECURSIVE\",\"REFERENCES\",\"REGEXP\",\"RELEASE\",\"RENAME\",\"REPEAT\",\"REPLACE\",\"REQUIRE\",\"RESTRICT\",\"REVOKE\",\"RIGHT\",\"RLIKE\",\"ROW\",\"ROWS\",\"SECOND_MICROSECOND\",\"SELECT\",\"SET\",\"SHOW\",\"SPATIAL\",\"SQL\",\"SQLEXCEPTION\",\"SQLSTATE\",\"SQLWARNING\",\"SQL_BIG_RESULT\",\"SQL_CALC_FOUND_ROWS\",\"SQL_SMALL_RESULT\",\"SSL\",\"STARTING\",\"STATS_EXTENDED\",\"STORED\",\"STRAIGHT_JOIN\",\"TABLE\",\"TABLESAMPLE\",\"TERMINATED\",\"THEN\",\"TO\",\"TRAILING\",\"TRIGGER\",\"TRUE\",\"TiDB_CURRENT_TSO\",\"UNION\",\"UNIQUE\",\"UNLOCK\",\"UNSIGNED\",\"UNTIL\",\"UPDATE\",\"USAGE\",\"USE\",\"USING\",\"UTC_DATE\",\"UTC_TIME\",\"UTC_TIMESTAMP\",\"VALUES\",\"VIRTUAL\",\"WHEN\",\"WHERE\",\"WHILE\",\"WINDOW\",\"WITH\",\"WRITE\",\"XOR\",\"YEAR_MONTH\",\"ZEROFILL\"],LS=[\"BIGINT\",\"BINARY\",\"BIT\",\"BLOB\",\"BOOL\",\"BOOLEAN\",\"CHAR\",\"CHARACTER\",\"DATE\",\"DATETIME\",\"DEC\",\"DECIMAL\",\"DOUBLE PRECISION\",\"DOUBLE\",\"ENUM\",\"FIXED\",\"INT\",\"INT1\",\"INT2\",\"INT3\",\"INT4\",\"INT8\",\"INTEGER\",\"LONGBLOB\",\"LONGTEXT\",\"MEDIUMBLOB\",\"MEDIUMINT\",\"MIDDLEINT\",\"NATIONAL CHAR\",\"NATIONAL VARCHAR\",\"NUMERIC\",\"PRECISION\",\"SMALLINT\",\"TEXT\",\"TIME\",\"TIMESTAMP\",\"TINYBLOB\",\"TINYINT\",\"TINYTEXT\",\"VARBINARY\",\"VARCHAR\",\"VARCHARACTER\",\"VARYING\",\"YEAR\"],CS=[\"ABS\",\"ACOS\",\"ADDDATE\",\"ADDTIME\",\"AES_DECRYPT\",\"AES_ENCRYPT\",\"ANY_VALUE\",\"ASCII\",\"ASIN\",\"ATAN\",\"ATAN2\",\"AVG\",\"BENCHMARK\",\"BIN\",\"BIN_TO_UUID\",\"BIT_AND\",\"BIT_COUNT\",\"BIT_LENGTH\",\"BIT_OR\",\"BIT_XOR\",\"BITAND\",\"BITNEG\",\"BITOR\",\"BITXOR\",\"CASE\",\"CAST\",\"CEIL\",\"CEILING\",\"CHAR_FUNC\",\"CHAR_LENGTH\",\"CHARACTER_LENGTH\",\"CHARSET\",\"COALESCE\",\"COERCIBILITY\",\"COLLATION\",\"COMPRESS\",\"CONCAT\",\"CONCAT_WS\",\"CONNECTION_ID\",\"CONV\",\"CONVERT\",\"CONVERT_TZ\",\"COS\",\"COT\",\"COUNT\",\"CRC32\",\"CUME_DIST\",\"CURDATE\",\"CURRENT_DATE\",\"CURRENT_RESOURCE_GROUP\",\"CURRENT_ROLE\",\"CURRENT_TIME\",\"CURRENT_TIMESTAMP\",\"CURRENT_USER\",\"CURTIME\",\"DATABASE\",\"DATE\",\"DATE_ADD\",\"DATE_FORMAT\",\"DATE_SUB\",\"DATEDIFF\",\"DAY\",\"DAYNAME\",\"DAYOFMONTH\",\"DAYOFWEEK\",\"DAYOFYEAR\",\"DECODE\",\"DEFAULT_FUNC\",\"DEGREES\",\"DENSE_RANK\",\"DES_DECRYPT\",\"DES_ENCRYPT\",\"DIV\",\"ELT\",\"ENCODE\",\"ENCRYPT\",\"EQ\",\"EXP\",\"EXPORT_SET\",\"EXTRACT\",\"FIELD\",\"FIND_IN_SET\",\"FIRST_VALUE\",\"FLOOR\",\"FORMAT\",\"FORMAT_BYTES\",\"FORMAT_NANO_TIME\",\"FOUND_ROWS\",\"FROM_BASE64\",\"FROM_DAYS\",\"FROM_UNIXTIME\",\"GE\",\"GET_FORMAT\",\"GET_LOCK\",\"GETPARAM\",\"GREATEST\",\"GROUP_CONCAT\",\"GROUPING\",\"GT\",\"HEX\",\"HOUR\",\"IF\",\"IFNULL\",\"ILIKE\",\"INET6_ATON\",\"INET6_NTOA\",\"INET_ATON\",\"INET_NTOA\",\"INSERT_FUNC\",\"INSTR\",\"INTDIV\",\"INTERVAL\",\"IS_FREE_LOCK\",\"IS_IPV4\",\"IS_IPV4_COMPAT\",\"IS_IPV4_MAPPED\",\"IS_IPV6\",\"IS_USED_LOCK\",\"IS_UUID\",\"ISFALSE\",\"ISNULL\",\"ISTRUE\",\"JSON_ARRAY\",\"JSON_ARRAYAGG\",\"JSON_ARRAY_APPEND\",\"JSON_ARRAY_INSERT\",\"JSON_CONTAINS\",\"JSON_CONTAINS_PATH\",\"JSON_DEPTH\",\"JSON_EXTRACT\",\"JSON_INSERT\",\"JSON_KEYS\",\"JSON_LENGTH\",\"JSON_MEMBEROF\",\"JSON_MERGE\",\"JSON_MERGE_PATCH\",\"JSON_MERGE_PRESERVE\",\"JSON_OBJECT\",\"JSON_OBJECTAGG\",\"JSON_OVERLAPS\",\"JSON_PRETTY\",\"JSON_QUOTE\",\"JSON_REMOVE\",\"JSON_REPLACE\",\"JSON_SEARCH\",\"JSON_SET\",\"JSON_STORAGE_FREE\",\"JSON_STORAGE_SIZE\",\"JSON_TYPE\",\"JSON_UNQUOTE\",\"JSON_VALID\",\"LAG\",\"LAST_DAY\",\"LAST_INSERT_ID\",\"LAST_VALUE\",\"LASTVAL\",\"LCASE\",\"LE\",\"LEAD\",\"LEAST\",\"LEFT\",\"LEFTSHIFT\",\"LENGTH\",\"LIKE\",\"LN\",\"LOAD_FILE\",\"LOCALTIME\",\"LOCALTIMESTAMP\",\"LOCATE\",\"LOG\",\"LOG10\",\"LOG2\",\"LOWER\",\"LPAD\",\"LT\",\"LTRIM\",\"MAKE_SET\",\"MAKEDATE\",\"MAKETIME\",\"MASTER_POS_WAIT\",\"MAX\",\"MD5\",\"MICROSECOND\",\"MID\",\"MIN\",\"MINUS\",\"MINUTE\",\"MOD\",\"MONTH\",\"MONTHNAME\",\"MUL\",\"NAME_CONST\",\"NE\",\"NEXTVAL\",\"NOT\",\"NOW\",\"NTH_VALUE\",\"NTILE\",\"NULLEQ\",\"OCT\",\"OCTET_LENGTH\",\"OLD_PASSWORD\",\"ORD\",\"PASSWORD_FUNC\",\"PERCENT_RANK\",\"PERIOD_ADD\",\"PERIOD_DIFF\",\"PI\",\"PLUS\",\"POSITION\",\"POW\",\"POWER\",\"QUARTER\",\"QUOTE\",\"RADIANS\",\"RAND\",\"RANDOM_BYTES\",\"RANK\",\"REGEXP\",\"REGEXP_INSTR\",\"REGEXP_LIKE\",\"REGEXP_REPLACE\",\"REGEXP_SUBSTR\",\"RELEASE_ALL_LOCKS\",\"RELEASE_LOCK\",\"REPEAT\",\"REPLACE\",\"REVERSE\",\"RIGHT\",\"RIGHTSHIFT\",\"ROUND\",\"ROW_COUNT\",\"ROW_NUMBER\",\"RPAD\",\"RTRIM\",\"SCHEMA\",\"SEC_TO_TIME\",\"SECOND\",\"SESSION_USER\",\"SETVAL\",\"SETVAR\",\"SHA\",\"SHA1\",\"SHA2\",\"SIGN\",\"SIN\",\"SLEEP\",\"SM3\",\"SPACE\",\"SQRT\",\"STD\",\"STDDEV\",\"STDDEV_POP\",\"STDDEV_SAMP\",\"STR_TO_DATE\",\"STRCMP\",\"SUBDATE\",\"SUBSTR\",\"SUBSTRING\",\"SUBSTRING_INDEX\",\"SUBTIME\",\"SUM\",\"SYSDATE\",\"SYSTEM_USER\",\"TAN\",\"TIDB_BOUNDED_STALENESS\",\"TIDB_CURRENT_TSO\",\"TIDB_DECODE_BINARY_PLAN\",\"TIDB_DECODE_KEY\",\"TIDB_DECODE_PLAN\",\"TIDB_DECODE_SQL_DIGESTS\",\"TIDB_ENCODE_SQL_DIGEST\",\"TIDB_IS_DDL_OWNER\",\"TIDB_PARSE_TSO\",\"TIDB_PARSE_TSO_LOGICAL\",\"TIDB_ROW_CHECKSUM\",\"TIDB_SHARD\",\"TIDB_VERSION\",\"TIME\",\"TIME_FORMAT\",\"TIME_TO_SEC\",\"TIMEDIFF\",\"TIMESTAMP\",\"TIMESTAMPADD\",\"TIMESTAMPDIFF\",\"TO_BASE64\",\"TO_DAYS\",\"TO_SECONDS\",\"TRANSLATE\",\"TRIM\",\"TRUNCATE\",\"UCASE\",\"UNARYMINUS\",\"UNCOMPRESS\",\"UNCOMPRESSED_LENGTH\",\"UNHEX\",\"UNIX_TIMESTAMP\",\"UPPER\",\"UTC_DATE\",\"UTC_TIME\",\"UTC_TIMESTAMP\",\"UUID\",\"UUID_SHORT\",\"UUID_TO_BIN\",\"VALIDATE_PASSWORD_STRENGTH\",\"VAR_POP\",\"VAR_SAMP\",\"VARIANCE\",\"VERSION\",\"VITESS_HASH\",\"WEEK\",\"WEEKDAY\",\"WEEKOFYEAR\",\"WEIGHT_STRING\",\"YEAR\",\"YEARWEEK\"],uS=v([\"SELECT [ALL | DISTINCT | DISTINCTROW]\"]),cS=v([\"WITH [RECURSIVE]\",\"FROM\",\"WHERE\",\"GROUP BY\",\"HAVING\",\"WINDOW\",\"PARTITION BY\",\"ORDER BY\",\"LIMIT\",\"OFFSET\",\"INSERT [LOW_PRIORITY | DELAYED | HIGH_PRIORITY] [IGNORE] [INTO]\",\"REPLACE [LOW_PRIORITY | DELAYED] [INTO]\",\"VALUES\",\"ON DUPLICATE KEY UPDATE\",\"SET\"]),Sr=v([\"CREATE [TEMPORARY] TABLE [IF NOT EXISTS]\"]),Qt=v([\"CREATE [OR REPLACE] [SQL SECURITY DEFINER | SQL SECURITY INVOKER] VIEW [IF NOT EXISTS]\",\"UPDATE [LOW_PRIORITY] [IGNORE]\",\"DELETE [LOW_PRIORITY] [QUICK] [IGNORE] FROM\",\"DROP [TEMPORARY] TABLE [IF EXISTS]\",\"ALTER TABLE\",\"ADD [COLUMN]\",\"{CHANGE | MODIFY} [COLUMN]\",\"DROP [COLUMN]\",\"RENAME [TO | AS]\",\"RENAME COLUMN\",\"ALTER [COLUMN]\",\"{SET | DROP} DEFAULT\",\"TRUNCATE [TABLE]\",\"ALTER DATABASE\",\"ALTER INSTANCE\",\"ALTER RESOURCE GROUP\",\"ALTER SEQUENCE\",\"ALTER USER\",\"ALTER VIEW\",\"ANALYZE TABLE\",\"CHECK TABLE\",\"CHECKSUM TABLE\",\"COMMIT\",\"CREATE DATABASE\",\"CREATE INDEX\",\"CREATE RESOURCE GROUP\",\"CREATE ROLE\",\"CREATE SEQUENCE\",\"CREATE USER\",\"DEALLOCATE PREPARE\",\"DESCRIBE\",\"DROP DATABASE\",\"DROP INDEX\",\"DROP RESOURCE GROUP\",\"DROP ROLE\",\"DROP TABLESPACE\",\"DROP USER\",\"DROP VIEW\",\"EXPLAIN\",\"FLUSH\",\"GRANT\",\"IMPORT TABLE\",\"INSTALL COMPONENT\",\"INSTALL PLUGIN\",\"KILL\",\"LOAD DATA\",\"LOCK INSTANCE FOR BACKUP\",\"LOCK TABLES\",\"OPTIMIZE TABLE\",\"PREPARE\",\"RELEASE SAVEPOINT\",\"RENAME TABLE\",\"RENAME USER\",\"REPAIR TABLE\",\"RESET\",\"REVOKE\",\"ROLLBACK\",\"ROLLBACK TO SAVEPOINT\",\"SAVEPOINT\",\"SET CHARACTER SET\",\"SET DEFAULT ROLE\",\"SET NAMES\",\"SET PASSWORD\",\"SET RESOURCE GROUP\",\"SET ROLE\",\"SET TRANSACTION\",\"SHOW\",\"SHOW BINARY LOGS\",\"SHOW BINLOG EVENTS\",\"SHOW CHARACTER SET\",\"SHOW COLLATION\",\"SHOW COLUMNS\",\"SHOW CREATE DATABASE\",\"SHOW CREATE TABLE\",\"SHOW CREATE USER\",\"SHOW CREATE VIEW\",\"SHOW DATABASES\",\"SHOW ENGINE\",\"SHOW ENGINES\",\"SHOW ERRORS\",\"SHOW EVENTS\",\"SHOW GRANTS\",\"SHOW INDEX\",\"SHOW MASTER STATUS\",\"SHOW OPEN TABLES\",\"SHOW PLUGINS\",\"SHOW PRIVILEGES\",\"SHOW PROCESSLIST\",\"SHOW PROFILE\",\"SHOW PROFILES\",\"SHOW STATUS\",\"SHOW TABLE STATUS\",\"SHOW TABLES\",\"SHOW TRIGGERS\",\"SHOW VARIABLES\",\"SHOW WARNINGS\",\"TABLE\",\"UNINSTALL COMPONENT\",\"UNINSTALL PLUGIN\",\"UNLOCK INSTANCE\",\"UNLOCK TABLES\",\"USE\"]),fS=v([\"UNION [ALL | DISTINCT]\"]),PS=v([\"JOIN\",\"{LEFT | RIGHT} [OUTER] JOIN\",\"{INNER | CROSS} JOIN\",\"NATURAL [INNER] JOIN\",\"NATURAL {LEFT | RIGHT} [OUTER] JOIN\",\"STRAIGHT_JOIN\"]),DS=v([\"ON {UPDATE | DELETE} [SET NULL]\",\"CHARACTER SET\",\"{ROWS | RANGE} BETWEEN\",\"IDENTIFIED BY\"]),dS={name:\"tidb\",tokenizerOptions:{reservedSelect:uS,reservedClauses:[...cS,...Sr,...Qt],reservedSetOperations:fS,reservedJoins:PS,reservedPhrases:DS,supportsXor:!0,reservedKeywords:_S,reservedDataTypes:LS,reservedFunctionNames:CS,stringTypes:['\"\"-qq-bs',{quote:\"''-qq-bs\",prefixes:[\"N\"]},{quote:\"''-raw\",prefixes:[\"B\",\"X\"],requirePrefix:!0}],identTypes:[\"``\"],identChars:{first:\"$\",rest:\"$\",allowFirstCharNumber:!0},variableTypes:[{regex:\"@@?[A-Za-z0-9_.$]+\"},{quote:'\"\"-qq-bs',prefixes:[\"@\"],requirePrefix:!0},{quote:\"''-qq-bs\",prefixes:[\"@\"],requirePrefix:!0},{quote:\"``\",prefixes:[\"@\"],requirePrefix:!0}],paramTypes:{positional:!0},lineCommentTypes:[\"--\",\"#\"],operators:[\"%\",\":=\",\"&\",\"|\",\"^\",\"~\",\"<<\",\">>\",\"<=>\",\"->\",\"->>\",\"&&\",\"||\",\"!\",\"*.*\"],postProcess:yt},formatOptions:{onelineClauses:[...Sr,...Qt],tabularOnelineClauses:Qt}},pS=[\"ABORT\",\"ABS\",\"ACOS\",\"ADVISOR\",\"ARRAY_AGG\",\"ARRAY_AGG\",\"ARRAY_APPEND\",\"ARRAY_AVG\",\"ARRAY_BINARY_SEARCH\",\"ARRAY_CONCAT\",\"ARRAY_CONTAINS\",\"ARRAY_COUNT\",\"ARRAY_DISTINCT\",\"ARRAY_EXCEPT\",\"ARRAY_FLATTEN\",\"ARRAY_IFNULL\",\"ARRAY_INSERT\",\"ARRAY_INTERSECT\",\"ARRAY_LENGTH\",\"ARRAY_MAX\",\"ARRAY_MIN\",\"ARRAY_MOVE\",\"ARRAY_POSITION\",\"ARRAY_PREPEND\",\"ARRAY_PUT\",\"ARRAY_RANGE\",\"ARRAY_REMOVE\",\"ARRAY_REPEAT\",\"ARRAY_REPLACE\",\"ARRAY_REVERSE\",\"ARRAY_SORT\",\"ARRAY_STAR\",\"ARRAY_SUM\",\"ARRAY_SYMDIFF\",\"ARRAY_SYMDIFF1\",\"ARRAY_SYMDIFFN\",\"ARRAY_UNION\",\"ASIN\",\"ATAN\",\"ATAN2\",\"AVG\",\"BASE64\",\"BASE64_DECODE\",\"BASE64_ENCODE\",\"BITAND \",\"BITCLEAR \",\"BITNOT \",\"BITOR \",\"BITSET \",\"BITSHIFT \",\"BITTEST \",\"BITXOR \",\"CEIL\",\"CLOCK_LOCAL\",\"CLOCK_MILLIS\",\"CLOCK_STR\",\"CLOCK_TZ\",\"CLOCK_UTC\",\"COALESCE\",\"CONCAT\",\"CONCAT2\",\"CONTAINS\",\"CONTAINS_TOKEN\",\"CONTAINS_TOKEN_LIKE\",\"CONTAINS_TOKEN_REGEXP\",\"COS\",\"COUNT\",\"COUNT\",\"COUNTN\",\"CUME_DIST\",\"CURL\",\"DATE_ADD_MILLIS\",\"DATE_ADD_STR\",\"DATE_DIFF_MILLIS\",\"DATE_DIFF_STR\",\"DATE_FORMAT_STR\",\"DATE_PART_MILLIS\",\"DATE_PART_STR\",\"DATE_RANGE_MILLIS\",\"DATE_RANGE_STR\",\"DATE_TRUNC_MILLIS\",\"DATE_TRUNC_STR\",\"DECODE\",\"DECODE_JSON\",\"DEGREES\",\"DENSE_RANK\",\"DURATION_TO_STR\",\"ENCODED_SIZE\",\"ENCODE_JSON\",\"EXP\",\"FIRST_VALUE\",\"FLOOR\",\"GREATEST\",\"HAS_TOKEN\",\"IFINF\",\"IFMISSING\",\"IFMISSINGORNULL\",\"IFNAN\",\"IFNANORINF\",\"IFNULL\",\"INITCAP\",\"ISARRAY\",\"ISATOM\",\"ISBITSET\",\"ISBOOLEAN\",\"ISNUMBER\",\"ISOBJECT\",\"ISSTRING\",\"LAG\",\"LAST_VALUE\",\"LEAD\",\"LEAST\",\"LENGTH\",\"LN\",\"LOG\",\"LOWER\",\"LTRIM\",\"MAX\",\"MEAN\",\"MEDIAN\",\"META\",\"MILLIS\",\"MILLIS_TO_LOCAL\",\"MILLIS_TO_STR\",\"MILLIS_TO_TZ\",\"MILLIS_TO_UTC\",\"MILLIS_TO_ZONE_NAME\",\"MIN\",\"MISSINGIF\",\"NANIF\",\"NEGINFIF\",\"NOW_LOCAL\",\"NOW_MILLIS\",\"NOW_STR\",\"NOW_TZ\",\"NOW_UTC\",\"NTH_VALUE\",\"NTILE\",\"NULLIF\",\"NVL\",\"NVL2\",\"OBJECT_ADD\",\"OBJECT_CONCAT\",\"OBJECT_INNER_PAIRS\",\"OBJECT_INNER_VALUES\",\"OBJECT_LENGTH\",\"OBJECT_NAMES\",\"OBJECT_PAIRS\",\"OBJECT_PUT\",\"OBJECT_REMOVE\",\"OBJECT_RENAME\",\"OBJECT_REPLACE\",\"OBJECT_UNWRAP\",\"OBJECT_VALUES\",\"PAIRS\",\"PERCENT_RANK\",\"PI\",\"POLY_LENGTH\",\"POSINFIF\",\"POSITION\",\"POWER\",\"RADIANS\",\"RANDOM\",\"RANK\",\"RATIO_TO_REPORT\",\"REGEXP_CONTAINS\",\"REGEXP_LIKE\",\"REGEXP_MATCHES\",\"REGEXP_POSITION\",\"REGEXP_REPLACE\",\"REGEXP_SPLIT\",\"REGEX_CONTAINS\",\"REGEX_LIKE\",\"REGEX_MATCHES\",\"REGEX_POSITION\",\"REGEX_REPLACE\",\"REGEX_SPLIT\",\"REPEAT\",\"REPLACE\",\"REVERSE\",\"ROUND\",\"ROW_NUMBER\",\"RTRIM\",\"SEARCH\",\"SEARCH_META\",\"SEARCH_SCORE\",\"SIGN\",\"SIN\",\"SPLIT\",\"SQRT\",\"STDDEV\",\"STDDEV_POP\",\"STDDEV_SAMP\",\"STR_TO_DURATION\",\"STR_TO_MILLIS\",\"STR_TO_TZ\",\"STR_TO_UTC\",\"STR_TO_ZONE_NAME\",\"SUBSTR\",\"SUFFIXES\",\"SUM\",\"TAN\",\"TITLE\",\"TOARRAY\",\"TOATOM\",\"TOBOOLEAN\",\"TOKENS\",\"TOKENS\",\"TONUMBER\",\"TOOBJECT\",\"TOSTRING\",\"TRIM\",\"TRUNC\",\"UPPER\",\"UUID\",\"VARIANCE\",\"VARIANCE_POP\",\"VARIANCE_SAMP\",\"VAR_POP\",\"VAR_SAMP\",\"WEEKDAY_MILLIS\",\"WEEKDAY_STR\",\"CAST\"],MS=[\"ADVISE\",\"ALL\",\"ALTER\",\"ANALYZE\",\"AND\",\"ANY\",\"ARRAY\",\"AS\",\"ASC\",\"AT\",\"BEGIN\",\"BETWEEN\",\"BINARY\",\"BOOLEAN\",\"BREAK\",\"BUCKET\",\"BUILD\",\"BY\",\"CALL\",\"CASE\",\"CAST\",\"CLUSTER\",\"COLLATE\",\"COLLECTION\",\"COMMIT\",\"COMMITTED\",\"CONNECT\",\"CONTINUE\",\"CORRELATED\",\"COVER\",\"CREATE\",\"CURRENT\",\"DATABASE\",\"DATASET\",\"DATASTORE\",\"DECLARE\",\"DECREMENT\",\"DELETE\",\"DERIVED\",\"DESC\",\"DESCRIBE\",\"DISTINCT\",\"DO\",\"DROP\",\"EACH\",\"ELEMENT\",\"ELSE\",\"END\",\"EVERY\",\"EXCEPT\",\"EXCLUDE\",\"EXECUTE\",\"EXISTS\",\"EXPLAIN\",\"FALSE\",\"FETCH\",\"FILTER\",\"FIRST\",\"FLATTEN\",\"FLUSH\",\"FOLLOWING\",\"FOR\",\"FORCE\",\"FROM\",\"FTS\",\"FUNCTION\",\"GOLANG\",\"GRANT\",\"GROUP\",\"GROUPS\",\"GSI\",\"HASH\",\"HAVING\",\"IF\",\"IGNORE\",\"ILIKE\",\"IN\",\"INCLUDE\",\"INCREMENT\",\"INDEX\",\"INFER\",\"INLINE\",\"INNER\",\"INSERT\",\"INTERSECT\",\"INTO\",\"IS\",\"ISOLATION\",\"JAVASCRIPT\",\"JOIN\",\"KEY\",\"KEYS\",\"KEYSPACE\",\"KNOWN\",\"LANGUAGE\",\"LAST\",\"LEFT\",\"LET\",\"LETTING\",\"LEVEL\",\"LIKE\",\"LIMIT\",\"LSM\",\"MAP\",\"MAPPING\",\"MATCHED\",\"MATERIALIZED\",\"MERGE\",\"MINUS\",\"MISSING\",\"NAMESPACE\",\"NEST\",\"NL\",\"NO\",\"NOT\",\"NTH_VALUE\",\"NULL\",\"NULLS\",\"NUMBER\",\"OBJECT\",\"OFFSET\",\"ON\",\"OPTION\",\"OPTIONS\",\"OR\",\"ORDER\",\"OTHERS\",\"OUTER\",\"OVER\",\"PARSE\",\"PARTITION\",\"PASSWORD\",\"PATH\",\"POOL\",\"PRECEDING\",\"PREPARE\",\"PRIMARY\",\"PRIVATE\",\"PRIVILEGE\",\"PROBE\",\"PROCEDURE\",\"PUBLIC\",\"RANGE\",\"RAW\",\"REALM\",\"REDUCE\",\"RENAME\",\"RESPECT\",\"RETURN\",\"RETURNING\",\"REVOKE\",\"RIGHT\",\"ROLE\",\"ROLLBACK\",\"ROW\",\"ROWS\",\"SATISFIES\",\"SAVEPOINT\",\"SCHEMA\",\"SCOPE\",\"SELECT\",\"SELF\",\"SEMI\",\"SET\",\"SHOW\",\"SOME\",\"START\",\"STATISTICS\",\"STRING\",\"SYSTEM\",\"THEN\",\"TIES\",\"TO\",\"TRAN\",\"TRANSACTION\",\"TRIGGER\",\"TRUE\",\"TRUNCATE\",\"UNBOUNDED\",\"UNDER\",\"UNION\",\"UNIQUE\",\"UNKNOWN\",\"UNNEST\",\"UNSET\",\"UPDATE\",\"UPSERT\",\"USE\",\"USER\",\"USING\",\"VALIDATE\",\"VALUE\",\"VALUED\",\"VALUES\",\"VIA\",\"VIEW\",\"WHEN\",\"WHERE\",\"WHILE\",\"WINDOW\",\"WITH\",\"WITHIN\",\"WORK\",\"XOR\"],US=[],mS=v([\"SELECT [ALL | DISTINCT]\"]),hS=v([\"WITH\",\"FROM\",\"WHERE\",\"GROUP BY\",\"HAVING\",\"WINDOW\",\"PARTITION BY\",\"ORDER BY\",\"LIMIT\",\"OFFSET\",\"INSERT INTO\",\"VALUES\",\"SET\",\"MERGE INTO\",\"WHEN [NOT] MATCHED THEN\",\"UPDATE SET\",\"INSERT\",\"NEST\",\"UNNEST\",\"RETURNING\"]),or=v([\"UPDATE\",\"DELETE FROM\",\"SET SCHEMA\",\"ADVISE\",\"ALTER INDEX\",\"BEGIN TRANSACTION\",\"BUILD INDEX\",\"COMMIT TRANSACTION\",\"CREATE COLLECTION\",\"CREATE FUNCTION\",\"CREATE INDEX\",\"CREATE PRIMARY INDEX\",\"CREATE SCOPE\",\"DROP COLLECTION\",\"DROP FUNCTION\",\"DROP INDEX\",\"DROP PRIMARY INDEX\",\"DROP SCOPE\",\"EXECUTE\",\"EXECUTE FUNCTION\",\"EXPLAIN\",\"GRANT\",\"INFER\",\"PREPARE\",\"REVOKE\",\"ROLLBACK TRANSACTION\",\"SAVEPOINT\",\"SET TRANSACTION\",\"UPDATE STATISTICS\",\"UPSERT\",\"LET\",\"SET CURRENT SCHEMA\",\"SHOW\",\"USE [PRIMARY] KEYS\"]),GS=v([\"UNION [ALL]\",\"EXCEPT [ALL]\",\"INTERSECT [ALL]\"]),gS=v([\"JOIN\",\"{LEFT | RIGHT} [OUTER] JOIN\",\"INNER JOIN\"]),HS=v([\"{ROWS | RANGE | GROUPS} BETWEEN\"]),bS={name:\"n1ql\",tokenizerOptions:{reservedSelect:mS,reservedClauses:[...hS,...or],reservedSetOperations:GS,reservedJoins:gS,reservedPhrases:HS,supportsXor:!0,reservedKeywords:MS,reservedDataTypes:US,reservedFunctionNames:pS,stringTypes:['\"\"-bs',\"''-bs\"],identTypes:[\"``\"],extraParens:[\"[]\",\"{}\"],paramTypes:{positional:!0,numbered:[\"$\"],named:[\"$\"]},lineCommentTypes:[\"#\",\"--\"],operators:[\"%\",\"==\",\":\",\"||\"]},formatOptions:{onelineClauses:or}},yS=[\"ADD\",\"AGENT\",\"AGGREGATE\",\"ALL\",\"ALTER\",\"AND\",\"ANY\",\"ARROW\",\"AS\",\"ASC\",\"AT\",\"ATTRIBUTE\",\"AUTHID\",\"AVG\",\"BEGIN\",\"BETWEEN\",\"BLOCK\",\"BODY\",\"BOTH\",\"BOUND\",\"BULK\",\"BY\",\"BYTE\",\"CALL\",\"CALLING\",\"CASCADE\",\"CASE\",\"CHARSET\",\"CHARSETFORM\",\"CHARSETID\",\"CHECK\",\"CLOSE\",\"CLUSTER\",\"CLUSTERS\",\"COLAUTH\",\"COLLECT\",\"COLUMNS\",\"COMMENT\",\"COMMIT\",\"COMMITTED\",\"COMPILED\",\"COMPRESS\",\"CONNECT\",\"CONSTANT\",\"CONSTRUCTOR\",\"CONTEXT\",\"CONVERT\",\"COUNT\",\"CRASH\",\"CREATE\",\"CURRENT\",\"CURSOR\",\"CUSTOMDATUM\",\"DANGLING\",\"DATA\",\"DAY\",\"DECLARE\",\"DEFAULT\",\"DEFINE\",\"DELETE\",\"DESC\",\"DETERMINISTIC\",\"DISTINCT\",\"DROP\",\"DURATION\",\"ELEMENT\",\"ELSE\",\"ELSIF\",\"EMPTY\",\"END\",\"ESCAPE\",\"EXCEPT\",\"EXCEPTION\",\"EXCEPTIONS\",\"EXCLUSIVE\",\"EXECUTE\",\"EXISTS\",\"EXIT\",\"EXTERNAL\",\"FETCH\",\"FINAL\",\"FIXED\",\"FOR\",\"FORALL\",\"FORCE\",\"FORM\",\"FROM\",\"FUNCTION\",\"GENERAL\",\"GOTO\",\"GRANT\",\"GROUP\",\"HASH\",\"HAVING\",\"HEAP\",\"HIDDEN\",\"HOUR\",\"IDENTIFIED\",\"IF\",\"IMMEDIATE\",\"IN\",\"INCLUDING\",\"INDEX\",\"INDEXES\",\"INDICATOR\",\"INDICES\",\"INFINITE\",\"INSERT\",\"INSTANTIABLE\",\"INTERFACE\",\"INTERSECT\",\"INTERVAL\",\"INTO\",\"INVALIDATE\",\"IS\",\"ISOLATION\",\"JAVA\",\"LANGUAGE\",\"LARGE\",\"LEADING\",\"LENGTH\",\"LEVEL\",\"LIBRARY\",\"LIKE\",\"LIKE2\",\"LIKE4\",\"LIKEC\",\"LIMIT\",\"LIMITED\",\"LOCAL\",\"LOCK\",\"LOOP\",\"MAP\",\"MAX\",\"MAXLEN\",\"MEMBER\",\"MERGE\",\"MIN\",\"MINUS\",\"MINUTE\",\"MOD\",\"MODE\",\"MODIFY\",\"MONTH\",\"MULTISET\",\"NAME\",\"NAN\",\"NATIONAL\",\"NATIVE\",\"NEW\",\"NOCOMPRESS\",\"NOCOPY\",\"NOT\",\"NOWAIT\",\"NULL\",\"OBJECT\",\"OCICOLL\",\"OCIDATE\",\"OCIDATETIME\",\"OCIDURATION\",\"OCIINTERVAL\",\"OCILOBLOCATOR\",\"OCINUMBER\",\"OCIRAW\",\"OCIREF\",\"OCIREFCURSOR\",\"OCIROWID\",\"OCISTRING\",\"OCITYPE\",\"OF\",\"ON\",\"ONLY\",\"OPAQUE\",\"OPEN\",\"OPERATOR\",\"OPTION\",\"OR\",\"ORACLE\",\"ORADATA\",\"ORDER\",\"OVERLAPS\",\"ORGANIZATION\",\"ORLANY\",\"ORLVARY\",\"OTHERS\",\"OUT\",\"OVERRIDING\",\"PACKAGE\",\"PARALLEL_ENABLE\",\"PARAMETER\",\"PARAMETERS\",\"PARTITION\",\"PASCAL\",\"PIPE\",\"PIPELINED\",\"PRAGMA\",\"PRIOR\",\"PRIVATE\",\"PROCEDURE\",\"PUBLIC\",\"RAISE\",\"RANGE\",\"READ\",\"RECORD\",\"REF\",\"REFERENCE\",\"REM\",\"REMAINDER\",\"RENAME\",\"RESOURCE\",\"RESULT\",\"RETURN\",\"RETURNING\",\"REVERSE\",\"REVOKE\",\"ROLLBACK\",\"ROW\",\"SAMPLE\",\"SAVE\",\"SAVEPOINT\",\"SB1\",\"SB2\",\"SB4\",\"SECOND\",\"SEGMENT\",\"SELECT\",\"SELF\",\"SEPARATE\",\"SEQUENCE\",\"SERIALIZABLE\",\"SET\",\"SHARE\",\"SHORT\",\"SIZE\",\"SIZE_T\",\"SOME\",\"SPARSE\",\"SQL\",\"SQLCODE\",\"SQLDATA\",\"SQLNAME\",\"SQLSTATE\",\"STANDARD\",\"START\",\"STATIC\",\"STDDEV\",\"STORED\",\"STRING\",\"STRUCT\",\"STYLE\",\"SUBMULTISET\",\"SUBPARTITION\",\"SUBSTITUTABLE\",\"SUBTYPE\",\"SUM\",\"SYNONYM\",\"TABAUTH\",\"TABLE\",\"TDO\",\"THE\",\"THEN\",\"TIME\",\"TIMEZONE_ABBR\",\"TIMEZONE_HOUR\",\"TIMEZONE_MINUTE\",\"TIMEZONE_REGION\",\"TO\",\"TRAILING\",\"TRANSAC\",\"TRANSACTIONAL\",\"TRUSTED\",\"TYPE\",\"UB1\",\"UB2\",\"UB4\",\"UNDER\",\"UNION\",\"UNIQUE\",\"UNSIGNED\",\"UNTRUSTED\",\"UPDATE\",\"USE\",\"USING\",\"VALIST\",\"VALUE\",\"VALUES\",\"VARIABLE\",\"VARIANCE\",\"VARRAY\",\"VIEW\",\"VIEWS\",\"VOID\",\"WHEN\",\"WHERE\",\"WHILE\",\"WITH\",\"WORK\",\"WRAPPED\",\"WRITE\",\"YEAR\",\"ZONE\"],BS=[\"ARRAY\",\"BFILE_BASE\",\"BINARY\",\"BLOB_BASE\",\"CHAR VARYING\",\"CHAR_BASE\",\"CHAR\",\"CHARACTER VARYING\",\"CHARACTER\",\"CLOB_BASE\",\"DATE_BASE\",\"DATE\",\"DECIMAL\",\"DOUBLE\",\"FLOAT\",\"INT\",\"INTERVAL DAY\",\"INTERVAL YEAR\",\"LONG\",\"NATIONAL CHAR VARYING\",\"NATIONAL CHAR\",\"NATIONAL CHARACTER VARYING\",\"NATIONAL CHARACTER\",\"NCHAR VARYING\",\"NCHAR\",\"NCHAR\",\"NUMBER_BASE\",\"NUMBER\",\"NUMBERIC\",\"NVARCHAR\",\"PRECISION\",\"RAW\",\"TIMESTAMP\",\"UROWID\",\"VARCHAR\",\"VARCHAR2\"],vS=[\"ABS\",\"ACOS\",\"ASIN\",\"ATAN\",\"ATAN2\",\"BITAND\",\"CEIL\",\"COS\",\"COSH\",\"EXP\",\"FLOOR\",\"LN\",\"LOG\",\"MOD\",\"NANVL\",\"POWER\",\"REMAINDER\",\"ROUND\",\"SIGN\",\"SIN\",\"SINH\",\"SQRT\",\"TAN\",\"TANH\",\"TRUNC\",\"WIDTH_BUCKET\",\"CHR\",\"CONCAT\",\"INITCAP\",\"LOWER\",\"LPAD\",\"LTRIM\",\"NLS_INITCAP\",\"NLS_LOWER\",\"NLSSORT\",\"NLS_UPPER\",\"REGEXP_REPLACE\",\"REGEXP_SUBSTR\",\"REPLACE\",\"RPAD\",\"RTRIM\",\"SOUNDEX\",\"SUBSTR\",\"TRANSLATE\",\"TREAT\",\"TRIM\",\"UPPER\",\"NLS_CHARSET_DECL_LEN\",\"NLS_CHARSET_ID\",\"NLS_CHARSET_NAME\",\"ASCII\",\"INSTR\",\"LENGTH\",\"REGEXP_INSTR\",\"ADD_MONTHS\",\"CURRENT_DATE\",\"CURRENT_TIMESTAMP\",\"DBTIMEZONE\",\"EXTRACT\",\"FROM_TZ\",\"LAST_DAY\",\"LOCALTIMESTAMP\",\"MONTHS_BETWEEN\",\"NEW_TIME\",\"NEXT_DAY\",\"NUMTODSINTERVAL\",\"NUMTOYMINTERVAL\",\"ROUND\",\"SESSIONTIMEZONE\",\"SYS_EXTRACT_UTC\",\"SYSDATE\",\"SYSTIMESTAMP\",\"TO_CHAR\",\"TO_TIMESTAMP\",\"TO_TIMESTAMP_TZ\",\"TO_DSINTERVAL\",\"TO_YMINTERVAL\",\"TRUNC\",\"TZ_OFFSET\",\"GREATEST\",\"LEAST\",\"ASCIISTR\",\"BIN_TO_NUM\",\"CAST\",\"CHARTOROWID\",\"COMPOSE\",\"CONVERT\",\"DECOMPOSE\",\"HEXTORAW\",\"NUMTODSINTERVAL\",\"NUMTOYMINTERVAL\",\"RAWTOHEX\",\"RAWTONHEX\",\"ROWIDTOCHAR\",\"ROWIDTONCHAR\",\"SCN_TO_TIMESTAMP\",\"TIMESTAMP_TO_SCN\",\"TO_BINARY_DOUBLE\",\"TO_BINARY_FLOAT\",\"TO_CHAR\",\"TO_CLOB\",\"TO_DATE\",\"TO_DSINTERVAL\",\"TO_LOB\",\"TO_MULTI_BYTE\",\"TO_NCHAR\",\"TO_NCLOB\",\"TO_NUMBER\",\"TO_DSINTERVAL\",\"TO_SINGLE_BYTE\",\"TO_TIMESTAMP\",\"TO_TIMESTAMP_TZ\",\"TO_YMINTERVAL\",\"TO_YMINTERVAL\",\"TRANSLATE\",\"UNISTR\",\"BFILENAME\",\"EMPTY_BLOB,\",\"EMPTY_CLOB\",\"CARDINALITY\",\"COLLECT\",\"POWERMULTISET\",\"POWERMULTISET_BY_CARDINALITY\",\"SET\",\"SYS_CONNECT_BY_PATH\",\"CLUSTER_ID\",\"CLUSTER_PROBABILITY\",\"CLUSTER_SET\",\"FEATURE_ID\",\"FEATURE_SET\",\"FEATURE_VALUE\",\"PREDICTION\",\"PREDICTION_COST\",\"PREDICTION_DETAILS\",\"PREDICTION_PROBABILITY\",\"PREDICTION_SET\",\"APPENDCHILDXML\",\"DELETEXML\",\"DEPTH\",\"EXTRACT\",\"EXISTSNODE\",\"EXTRACTVALUE\",\"INSERTCHILDXML\",\"INSERTXMLBEFORE\",\"PATH\",\"SYS_DBURIGEN\",\"SYS_XMLAGG\",\"SYS_XMLGEN\",\"UPDATEXML\",\"XMLAGG\",\"XMLCDATA\",\"XMLCOLATTVAL\",\"XMLCOMMENT\",\"XMLCONCAT\",\"XMLFOREST\",\"XMLPARSE\",\"XMLPI\",\"XMLQUERY\",\"XMLROOT\",\"XMLSEQUENCE\",\"XMLSERIALIZE\",\"XMLTABLE\",\"XMLTRANSFORM\",\"DECODE\",\"DUMP\",\"ORA_HASH\",\"VSIZE\",\"COALESCE\",\"LNNVL\",\"NULLIF\",\"NVL\",\"NVL2\",\"SYS_CONTEXT\",\"SYS_GUID\",\"SYS_TYPEID\",\"UID\",\"USER\",\"USERENV\",\"AVG\",\"COLLECT\",\"CORR\",\"CORR_S\",\"CORR_K\",\"COUNT\",\"COVAR_POP\",\"COVAR_SAMP\",\"CUME_DIST\",\"DENSE_RANK\",\"FIRST\",\"GROUP_ID\",\"GROUPING\",\"GROUPING_ID\",\"LAST\",\"MAX\",\"MEDIAN\",\"MIN\",\"PERCENTILE_CONT\",\"PERCENTILE_DISC\",\"PERCENT_RANK\",\"RANK\",\"REGR_SLOPE\",\"REGR_INTERCEPT\",\"REGR_COUNT\",\"REGR_R2\",\"REGR_AVGX\",\"REGR_AVGY\",\"REGR_SXX\",\"REGR_SYY\",\"REGR_SXY\",\"STATS_BINOMIAL_TEST\",\"STATS_CROSSTAB\",\"STATS_F_TEST\",\"STATS_KS_TEST\",\"STATS_MODE\",\"STATS_MW_TEST\",\"STATS_ONE_WAY_ANOVA\",\"STATS_T_TEST_ONE\",\"STATS_T_TEST_PAIRED\",\"STATS_T_TEST_INDEP\",\"STATS_T_TEST_INDEPU\",\"STATS_WSR_TEST\",\"STDDEV\",\"STDDEV_POP\",\"STDDEV_SAMP\",\"SUM\",\"VAR_POP\",\"VAR_SAMP\",\"VARIANCE\",\"FIRST_VALUE\",\"LAG\",\"LAST_VALUE\",\"LEAD\",\"NTILE\",\"RATIO_TO_REPORT\",\"ROW_NUMBER\",\"DEREF\",\"MAKE_REF\",\"REF\",\"REFTOHEX\",\"VALUE\",\"CV\",\"ITERATION_NUMBER\",\"PRESENTNNV\",\"PRESENTV\",\"PREVIOUS\"],FS=v([\"SELECT [ALL | DISTINCT | UNIQUE]\"]),YS=v([\"WITH\",\"FROM\",\"WHERE\",\"GROUP BY\",\"HAVING\",\"PARTITION BY\",\"ORDER [SIBLINGS] BY\",\"OFFSET\",\"FETCH {FIRST | NEXT}\",\"FOR UPDATE [OF]\",\"INSERT [INTO | ALL INTO]\",\"VALUES\",\"SET\",\"MERGE [INTO]\",\"WHEN [NOT] MATCHED [THEN]\",\"UPDATE SET\",\"RETURNING\"]),Or=v([\"CREATE [GLOBAL TEMPORARY | PRIVATE TEMPORARY | SHARDED | DUPLICATED | IMMUTABLE BLOCKCHAIN | BLOCKCHAIN | IMMUTABLE] TABLE\"]),Zt=v([\"CREATE [OR REPLACE] [NO FORCE | FORCE] [EDITIONING | EDITIONABLE | EDITIONABLE EDITIONING | NONEDITIONABLE] VIEW\",\"CREATE MATERIALIZED VIEW\",\"UPDATE [ONLY]\",\"DELETE FROM [ONLY]\",\"DROP TABLE\",\"ALTER TABLE\",\"ADD\",\"DROP {COLUMN | UNUSED COLUMNS | COLUMNS CONTINUE}\",\"MODIFY\",\"RENAME TO\",\"RENAME COLUMN\",\"TRUNCATE TABLE\",\"SET SCHEMA\",\"BEGIN\",\"CONNECT BY\",\"DECLARE\",\"EXCEPT\",\"EXCEPTION\",\"LOOP\",\"START WITH\"]),VS=v([\"UNION [ALL]\",\"EXCEPT\",\"INTERSECT\"]),WS=v([\"JOIN\",\"{LEFT | RIGHT | FULL} [OUTER] JOIN\",\"{INNER | CROSS} JOIN\",\"NATURAL [INNER] JOIN\",\"NATURAL {LEFT | RIGHT | FULL} [OUTER] JOIN\",\"{CROSS | OUTER} APPLY\"]),wS=v([\"ON {UPDATE | DELETE} [SET NULL]\",\"ON COMMIT\",\"{ROWS | RANGE} BETWEEN\"]),$S={name:\"plsql\",tokenizerOptions:{reservedSelect:FS,reservedClauses:[...YS,...Or,...Zt],reservedSetOperations:VS,reservedJoins:WS,reservedPhrases:wS,supportsXor:!0,reservedKeywords:yS,reservedDataTypes:BS,reservedFunctionNames:vS,stringTypes:[{quote:\"''-qq\",prefixes:[\"N\"]},{quote:\"q''\",prefixes:[\"N\"]}],identTypes:['\"\"-qq'],identChars:{rest:\"$#\"},variableTypes:[{regex:\"&{1,2}[A-Za-z][A-Za-z0-9_$#]*\"}],paramTypes:{numbered:[\":\"],named:[\":\"]},paramChars:{},operators:[\"**\",\":=\",\"%\",\"~=\",\"^=\",\">>\",\"<<\",\"=>\",\"@\",\"||\"],postProcess:xS},formatOptions:{alwaysDenseOperators:[\"@\"],onelineClauses:[...Or,...Zt],tabularOnelineClauses:Zt}};function xS(E){let e=tt;return E.map(T=>FE.SET(T)&&FE.BY(e)?AE(EE({},T),{type:\"RESERVED_KEYWORD\"}):(wR(T.type)&&(e=T),T))}var XS=[\"ABS\",\"ACOS\",\"ACOSD\",\"ACOSH\",\"ASIN\",\"ASIND\",\"ASINH\",\"ATAN\",\"ATAN2\",\"ATAN2D\",\"ATAND\",\"ATANH\",\"CBRT\",\"CEIL\",\"CEILING\",\"COS\",\"COSD\",\"COSH\",\"COT\",\"COTD\",\"DEGREES\",\"DIV\",\"EXP\",\"FACTORIAL\",\"FLOOR\",\"GCD\",\"LCM\",\"LN\",\"LOG\",\"LOG10\",\"MIN_SCALE\",\"MOD\",\"PI\",\"POWER\",\"RADIANS\",\"RANDOM\",\"ROUND\",\"SCALE\",\"SETSEED\",\"SIGN\",\"SIN\",\"SIND\",\"SINH\",\"SQRT\",\"TAN\",\"TAND\",\"TANH\",\"TRIM_SCALE\",\"TRUNC\",\"WIDTH_BUCKET\",\"ABS\",\"ASCII\",\"BIT_LENGTH\",\"BTRIM\",\"CHARACTER_LENGTH\",\"CHAR_LENGTH\",\"CHR\",\"CONCAT\",\"CONCAT_WS\",\"FORMAT\",\"INITCAP\",\"LEFT\",\"LENGTH\",\"LOWER\",\"LPAD\",\"LTRIM\",\"MD5\",\"NORMALIZE\",\"OCTET_LENGTH\",\"OVERLAY\",\"PARSE_IDENT\",\"PG_CLIENT_ENCODING\",\"POSITION\",\"QUOTE_IDENT\",\"QUOTE_LITERAL\",\"QUOTE_NULLABLE\",\"REGEXP_MATCH\",\"REGEXP_MATCHES\",\"REGEXP_REPLACE\",\"REGEXP_SPLIT_TO_ARRAY\",\"REGEXP_SPLIT_TO_TABLE\",\"REPEAT\",\"REPLACE\",\"REVERSE\",\"RIGHT\",\"RPAD\",\"RTRIM\",\"SPLIT_PART\",\"SPRINTF\",\"STARTS_WITH\",\"STRING_AGG\",\"STRING_TO_ARRAY\",\"STRING_TO_TABLE\",\"STRPOS\",\"SUBSTR\",\"SUBSTRING\",\"TO_ASCII\",\"TO_HEX\",\"TRANSLATE\",\"TRIM\",\"UNISTR\",\"UPPER\",\"BIT_COUNT\",\"BIT_LENGTH\",\"BTRIM\",\"CONVERT\",\"CONVERT_FROM\",\"CONVERT_TO\",\"DECODE\",\"ENCODE\",\"GET_BIT\",\"GET_BYTE\",\"LENGTH\",\"LTRIM\",\"MD5\",\"OCTET_LENGTH\",\"OVERLAY\",\"POSITION\",\"RTRIM\",\"SET_BIT\",\"SET_BYTE\",\"SHA224\",\"SHA256\",\"SHA384\",\"SHA512\",\"STRING_AGG\",\"SUBSTR\",\"SUBSTRING\",\"TRIM\",\"BIT_COUNT\",\"BIT_LENGTH\",\"GET_BIT\",\"LENGTH\",\"OCTET_LENGTH\",\"OVERLAY\",\"POSITION\",\"SET_BIT\",\"SUBSTRING\",\"REGEXP_MATCH\",\"REGEXP_MATCHES\",\"REGEXP_REPLACE\",\"REGEXP_SPLIT_TO_ARRAY\",\"REGEXP_SPLIT_TO_TABLE\",\"TO_CHAR\",\"TO_DATE\",\"TO_NUMBER\",\"TO_TIMESTAMP\",\"CLOCK_TIMESTAMP\",\"CURRENT_DATE\",\"CURRENT_TIME\",\"CURRENT_TIMESTAMP\",\"DATE_BIN\",\"DATE_PART\",\"DATE_TRUNC\",\"EXTRACT\",\"ISFINITE\",\"JUSTIFY_DAYS\",\"JUSTIFY_HOURS\",\"JUSTIFY_INTERVAL\",\"LOCALTIME\",\"LOCALTIMESTAMP\",\"MAKE_DATE\",\"MAKE_INTERVAL\",\"MAKE_TIME\",\"MAKE_TIMESTAMP\",\"MAKE_TIMESTAMPTZ\",\"NOW\",\"PG_SLEEP\",\"PG_SLEEP_FOR\",\"PG_SLEEP_UNTIL\",\"STATEMENT_TIMESTAMP\",\"TIMEOFDAY\",\"TO_TIMESTAMP\",\"TRANSACTION_TIMESTAMP\",\"ENUM_FIRST\",\"ENUM_LAST\",\"ENUM_RANGE\",\"AREA\",\"BOUND_BOX\",\"BOX\",\"CENTER\",\"CIRCLE\",\"DIAGONAL\",\"DIAMETER\",\"HEIGHT\",\"ISCLOSED\",\"ISOPEN\",\"LENGTH\",\"LINE\",\"LSEG\",\"NPOINTS\",\"PATH\",\"PCLOSE\",\"POINT\",\"POLYGON\",\"POPEN\",\"RADIUS\",\"SLOPE\",\"WIDTH\",\"ABBREV\",\"BROADCAST\",\"FAMILY\",\"HOST\",\"HOSTMASK\",\"INET_MERGE\",\"INET_SAME_FAMILY\",\"MACADDR8_SET7BIT\",\"MASKLEN\",\"NETMASK\",\"NETWORK\",\"SET_MASKLEN\",\"TRUNC\",\"ARRAY_TO_TSVECTOR\",\"GET_CURRENT_TS_CONFIG\",\"JSONB_TO_TSVECTOR\",\"JSON_TO_TSVECTOR\",\"LENGTH\",\"NUMNODE\",\"PHRASETO_TSQUERY\",\"PLAINTO_TSQUERY\",\"QUERYTREE\",\"SETWEIGHT\",\"STRIP\",\"TO_TSQUERY\",\"TO_TSVECTOR\",\"TSQUERY_PHRASE\",\"TSVECTOR_TO_ARRAY\",\"TS_DEBUG\",\"TS_DELETE\",\"TS_FILTER\",\"TS_HEADLINE\",\"TS_LEXIZE\",\"TS_PARSE\",\"TS_RANK\",\"TS_RANK_CD\",\"TS_REWRITE\",\"TS_STAT\",\"TS_TOKEN_TYPE\",\"WEBSEARCH_TO_TSQUERY\",\"UUID\",\"CURSOR_TO_XML\",\"CURSOR_TO_XMLSCHEMA\",\"DATABASE_TO_XML\",\"DATABASE_TO_XMLSCHEMA\",\"DATABASE_TO_XML_AND_XMLSCHEMA\",\"NEXTVAL\",\"QUERY_TO_XML\",\"QUERY_TO_XMLSCHEMA\",\"QUERY_TO_XML_AND_XMLSCHEMA\",\"SCHEMA_TO_XML\",\"SCHEMA_TO_XMLSCHEMA\",\"SCHEMA_TO_XML_AND_XMLSCHEMA\",\"STRING\",\"TABLE_TO_XML\",\"TABLE_TO_XMLSCHEMA\",\"TABLE_TO_XML_AND_XMLSCHEMA\",\"XMLAGG\",\"XMLCOMMENT\",\"XMLCONCAT\",\"XMLELEMENT\",\"XMLEXISTS\",\"XMLFOREST\",\"XMLPARSE\",\"XMLPI\",\"XMLROOT\",\"XMLSERIALIZE\",\"XMLTABLE\",\"XML_IS_WELL_FORMED\",\"XML_IS_WELL_FORMED_CONTENT\",\"XML_IS_WELL_FORMED_DOCUMENT\",\"XPATH\",\"XPATH_EXISTS\",\"ARRAY_TO_JSON\",\"JSONB_AGG\",\"JSONB_ARRAY_ELEMENTS\",\"JSONB_ARRAY_ELEMENTS_TEXT\",\"JSONB_ARRAY_LENGTH\",\"JSONB_BUILD_ARRAY\",\"JSONB_BUILD_OBJECT\",\"JSONB_EACH\",\"JSONB_EACH_TEXT\",\"JSONB_EXTRACT_PATH\",\"JSONB_EXTRACT_PATH_TEXT\",\"JSONB_INSERT\",\"JSONB_OBJECT\",\"JSONB_OBJECT_AGG\",\"JSONB_OBJECT_KEYS\",\"JSONB_PATH_EXISTS\",\"JSONB_PATH_EXISTS_TZ\",\"JSONB_PATH_MATCH\",\"JSONB_PATH_MATCH_TZ\",\"JSONB_PATH_QUERY\",\"JSONB_PATH_QUERY_ARRAY\",\"JSONB_PATH_QUERY_ARRAY_TZ\",\"JSONB_PATH_QUERY_FIRST\",\"JSONB_PATH_QUERY_FIRST_TZ\",\"JSONB_PATH_QUERY_TZ\",\"JSONB_POPULATE_RECORD\",\"JSONB_POPULATE_RECORDSET\",\"JSONB_PRETTY\",\"JSONB_SET\",\"JSONB_SET_LAX\",\"JSONB_STRIP_NULLS\",\"JSONB_TO_RECORD\",\"JSONB_TO_RECORDSET\",\"JSONB_TYPEOF\",\"JSON_AGG\",\"JSON_ARRAY_ELEMENTS\",\"JSON_ARRAY_ELEMENTS_TEXT\",\"JSON_ARRAY_LENGTH\",\"JSON_BUILD_ARRAY\",\"JSON_BUILD_OBJECT\",\"JSON_EACH\",\"JSON_EACH_TEXT\",\"JSON_EXTRACT_PATH\",\"JSON_EXTRACT_PATH_TEXT\",\"JSON_OBJECT\",\"JSON_OBJECT_AGG\",\"JSON_OBJECT_KEYS\",\"JSON_POPULATE_RECORD\",\"JSON_POPULATE_RECORDSET\",\"JSON_STRIP_NULLS\",\"JSON_TO_RECORD\",\"JSON_TO_RECORDSET\",\"JSON_TYPEOF\",\"ROW_TO_JSON\",\"TO_JSON\",\"TO_JSONB\",\"TO_TIMESTAMP\",\"CURRVAL\",\"LASTVAL\",\"NEXTVAL\",\"SETVAL\",\"COALESCE\",\"GREATEST\",\"LEAST\",\"NULLIF\",\"ARRAY_AGG\",\"ARRAY_APPEND\",\"ARRAY_CAT\",\"ARRAY_DIMS\",\"ARRAY_FILL\",\"ARRAY_LENGTH\",\"ARRAY_LOWER\",\"ARRAY_NDIMS\",\"ARRAY_POSITION\",\"ARRAY_POSITIONS\",\"ARRAY_PREPEND\",\"ARRAY_REMOVE\",\"ARRAY_REPLACE\",\"ARRAY_TO_STRING\",\"ARRAY_UPPER\",\"CARDINALITY\",\"STRING_TO_ARRAY\",\"TRIM_ARRAY\",\"UNNEST\",\"ISEMPTY\",\"LOWER\",\"LOWER_INC\",\"LOWER_INF\",\"MULTIRANGE\",\"RANGE_MERGE\",\"UPPER\",\"UPPER_INC\",\"UPPER_INF\",\"ARRAY_AGG\",\"AVG\",\"BIT_AND\",\"BIT_OR\",\"BIT_XOR\",\"BOOL_AND\",\"BOOL_OR\",\"COALESCE\",\"CORR\",\"COUNT\",\"COVAR_POP\",\"COVAR_SAMP\",\"CUME_DIST\",\"DENSE_RANK\",\"EVERY\",\"GROUPING\",\"JSONB_AGG\",\"JSONB_OBJECT_AGG\",\"JSON_AGG\",\"JSON_OBJECT_AGG\",\"MAX\",\"MIN\",\"MODE\",\"PERCENTILE_CONT\",\"PERCENTILE_DISC\",\"PERCENT_RANK\",\"RANGE_AGG\",\"RANGE_INTERSECT_AGG\",\"RANK\",\"REGR_AVGX\",\"REGR_AVGY\",\"REGR_COUNT\",\"REGR_INTERCEPT\",\"REGR_R2\",\"REGR_SLOPE\",\"REGR_SXX\",\"REGR_SXY\",\"REGR_SYY\",\"STDDEV\",\"STDDEV_POP\",\"STDDEV_SAMP\",\"STRING_AGG\",\"SUM\",\"TO_JSON\",\"TO_JSONB\",\"VARIANCE\",\"VAR_POP\",\"VAR_SAMP\",\"XMLAGG\",\"CUME_DIST\",\"DENSE_RANK\",\"FIRST_VALUE\",\"LAG\",\"LAST_VALUE\",\"LEAD\",\"NTH_VALUE\",\"NTILE\",\"PERCENT_RANK\",\"RANK\",\"ROW_NUMBER\",\"GENERATE_SERIES\",\"GENERATE_SUBSCRIPTS\",\"ACLDEFAULT\",\"ACLEXPLODE\",\"COL_DESCRIPTION\",\"CURRENT_CATALOG\",\"CURRENT_DATABASE\",\"CURRENT_QUERY\",\"CURRENT_ROLE\",\"CURRENT_SCHEMA\",\"CURRENT_SCHEMAS\",\"CURRENT_USER\",\"FORMAT_TYPE\",\"HAS_ANY_COLUMN_PRIVILEGE\",\"HAS_COLUMN_PRIVILEGE\",\"HAS_DATABASE_PRIVILEGE\",\"HAS_FOREIGN_DATA_WRAPPER_PRIVILEGE\",\"HAS_FUNCTION_PRIVILEGE\",\"HAS_LANGUAGE_PRIVILEGE\",\"HAS_SCHEMA_PRIVILEGE\",\"HAS_SEQUENCE_PRIVILEGE\",\"HAS_SERVER_PRIVILEGE\",\"HAS_TABLESPACE_PRIVILEGE\",\"HAS_TABLE_PRIVILEGE\",\"HAS_TYPE_PRIVILEGE\",\"INET_CLIENT_ADDR\",\"INET_CLIENT_PORT\",\"INET_SERVER_ADDR\",\"INET_SERVER_PORT\",\"MAKEACLITEM\",\"OBJ_DESCRIPTION\",\"PG_BACKEND_PID\",\"PG_BLOCKING_PIDS\",\"PG_COLLATION_IS_VISIBLE\",\"PG_CONF_LOAD_TIME\",\"PG_CONTROL_CHECKPOINT\",\"PG_CONTROL_INIT\",\"PG_CONTROL_SYSTEM\",\"PG_CONVERSION_IS_VISIBLE\",\"PG_CURRENT_LOGFILE\",\"PG_CURRENT_SNAPSHOT\",\"PG_CURRENT_XACT_ID\",\"PG_CURRENT_XACT_ID_IF_ASSIGNED\",\"PG_DESCRIBE_OBJECT\",\"PG_FUNCTION_IS_VISIBLE\",\"PG_GET_CATALOG_FOREIGN_KEYS\",\"PG_GET_CONSTRAINTDEF\",\"PG_GET_EXPR\",\"PG_GET_FUNCTIONDEF\",\"PG_GET_FUNCTION_ARGUMENTS\",\"PG_GET_FUNCTION_IDENTITY_ARGUMENTS\",\"PG_GET_FUNCTION_RESULT\",\"PG_GET_INDEXDEF\",\"PG_GET_KEYWORDS\",\"PG_GET_OBJECT_ADDRESS\",\"PG_GET_OWNED_SEQUENCE\",\"PG_GET_RULEDEF\",\"PG_GET_SERIAL_SEQUENCE\",\"PG_GET_STATISTICSOBJDEF\",\"PG_GET_TRIGGERDEF\",\"PG_GET_USERBYID\",\"PG_GET_VIEWDEF\",\"PG_HAS_ROLE\",\"PG_IDENTIFY_OBJECT\",\"PG_IDENTIFY_OBJECT_AS_ADDRESS\",\"PG_INDEXAM_HAS_PROPERTY\",\"PG_INDEX_COLUMN_HAS_PROPERTY\",\"PG_INDEX_HAS_PROPERTY\",\"PG_IS_OTHER_TEMP_SCHEMA\",\"PG_JIT_AVAILABLE\",\"PG_LAST_COMMITTED_XACT\",\"PG_LISTENING_CHANNELS\",\"PG_MY_TEMP_SCHEMA\",\"PG_NOTIFICATION_QUEUE_USAGE\",\"PG_OPCLASS_IS_VISIBLE\",\"PG_OPERATOR_IS_VISIBLE\",\"PG_OPFAMILY_IS_VISIBLE\",\"PG_OPTIONS_TO_TABLE\",\"PG_POSTMASTER_START_TIME\",\"PG_SAFE_SNAPSHOT_BLOCKING_PIDS\",\"PG_SNAPSHOT_XIP\",\"PG_SNAPSHOT_XMAX\",\"PG_SNAPSHOT_XMIN\",\"PG_STATISTICS_OBJ_IS_VISIBLE\",\"PG_TABLESPACE_DATABASES\",\"PG_TABLESPACE_LOCATION\",\"PG_TABLE_IS_VISIBLE\",\"PG_TRIGGER_DEPTH\",\"PG_TS_CONFIG_IS_VISIBLE\",\"PG_TS_DICT_IS_VISIBLE\",\"PG_TS_PARSER_IS_VISIBLE\",\"PG_TS_TEMPLATE_IS_VISIBLE\",\"PG_TYPEOF\",\"PG_TYPE_IS_VISIBLE\",\"PG_VISIBLE_IN_SNAPSHOT\",\"PG_XACT_COMMIT_TIMESTAMP\",\"PG_XACT_COMMIT_TIMESTAMP_ORIGIN\",\"PG_XACT_STATUS\",\"PQSERVERVERSION\",\"ROW_SECURITY_ACTIVE\",\"SESSION_USER\",\"SHOBJ_DESCRIPTION\",\"TO_REGCLASS\",\"TO_REGCOLLATION\",\"TO_REGNAMESPACE\",\"TO_REGOPER\",\"TO_REGOPERATOR\",\"TO_REGPROC\",\"TO_REGPROCEDURE\",\"TO_REGROLE\",\"TO_REGTYPE\",\"TXID_CURRENT\",\"TXID_CURRENT_IF_ASSIGNED\",\"TXID_CURRENT_SNAPSHOT\",\"TXID_SNAPSHOT_XIP\",\"TXID_SNAPSHOT_XMAX\",\"TXID_SNAPSHOT_XMIN\",\"TXID_STATUS\",\"TXID_VISIBLE_IN_SNAPSHOT\",\"USER\",\"VERSION\",\"BRIN_DESUMMARIZE_RANGE\",\"BRIN_SUMMARIZE_NEW_VALUES\",\"BRIN_SUMMARIZE_RANGE\",\"CONVERT_FROM\",\"CURRENT_SETTING\",\"GIN_CLEAN_PENDING_LIST\",\"PG_ADVISORY_LOCK\",\"PG_ADVISORY_LOCK_SHARED\",\"PG_ADVISORY_UNLOCK\",\"PG_ADVISORY_UNLOCK_ALL\",\"PG_ADVISORY_UNLOCK_SHARED\",\"PG_ADVISORY_XACT_LOCK\",\"PG_ADVISORY_XACT_LOCK_SHARED\",\"PG_BACKUP_START_TIME\",\"PG_CANCEL_BACKEND\",\"PG_COLLATION_ACTUAL_VERSION\",\"PG_COLUMN_COMPRESSION\",\"PG_COLUMN_SIZE\",\"PG_COPY_LOGICAL_REPLICATION_SLOT\",\"PG_COPY_PHYSICAL_REPLICATION_SLOT\",\"PG_CREATE_LOGICAL_REPLICATION_SLOT\",\"PG_CREATE_PHYSICAL_REPLICATION_SLOT\",\"PG_CREATE_RESTORE_POINT\",\"PG_CURRENT_WAL_FLUSH_LSN\",\"PG_CURRENT_WAL_INSERT_LSN\",\"PG_CURRENT_WAL_LSN\",\"PG_DATABASE_SIZE\",\"PG_DROP_REPLICATION_SLOT\",\"PG_EXPORT_SNAPSHOT\",\"PG_FILENODE_RELATION\",\"PG_GET_WAL_REPLAY_PAUSE_STATE\",\"PG_IMPORT_SYSTEM_COLLATIONS\",\"PG_INDEXES_SIZE\",\"PG_IS_IN_BACKUP\",\"PG_IS_IN_RECOVERY\",\"PG_IS_WAL_REPLAY_PAUSED\",\"PG_LAST_WAL_RECEIVE_LSN\",\"PG_LAST_WAL_REPLAY_LSN\",\"PG_LAST_XACT_REPLAY_TIMESTAMP\",\"PG_LOGICAL_EMIT_MESSAGE\",\"PG_LOGICAL_SLOT_GET_BINARY_CHANGES\",\"PG_LOGICAL_SLOT_GET_CHANGES\",\"PG_LOGICAL_SLOT_PEEK_BINARY_CHANGES\",\"PG_LOGICAL_SLOT_PEEK_CHANGES\",\"PG_LOG_BACKEND_MEMORY_CONTEXTS\",\"PG_LS_ARCHIVE_STATUSDIR\",\"PG_LS_DIR\",\"PG_LS_LOGDIR\",\"PG_LS_TMPDIR\",\"PG_LS_WALDIR\",\"PG_PARTITION_ANCESTORS\",\"PG_PARTITION_ROOT\",\"PG_PARTITION_TREE\",\"PG_PROMOTE\",\"PG_READ_BINARY_FILE\",\"PG_READ_FILE\",\"PG_RELATION_FILENODE\",\"PG_RELATION_FILEPATH\",\"PG_RELATION_SIZE\",\"PG_RELOAD_CONF\",\"PG_REPLICATION_ORIGIN_ADVANCE\",\"PG_REPLICATION_ORIGIN_CREATE\",\"PG_REPLICATION_ORIGIN_DROP\",\"PG_REPLICATION_ORIGIN_OID\",\"PG_REPLICATION_ORIGIN_PROGRESS\",\"PG_REPLICATION_ORIGIN_SESSION_IS_SETUP\",\"PG_REPLICATION_ORIGIN_SESSION_PROGRESS\",\"PG_REPLICATION_ORIGIN_SESSION_RESET\",\"PG_REPLICATION_ORIGIN_SESSION_SETUP\",\"PG_REPLICATION_ORIGIN_XACT_RESET\",\"PG_REPLICATION_ORIGIN_XACT_SETUP\",\"PG_REPLICATION_SLOT_ADVANCE\",\"PG_ROTATE_LOGFILE\",\"PG_SIZE_BYTES\",\"PG_SIZE_PRETTY\",\"PG_START_BACKUP\",\"PG_STAT_FILE\",\"PG_STOP_BACKUP\",\"PG_SWITCH_WAL\",\"PG_TABLESPACE_SIZE\",\"PG_TABLE_SIZE\",\"PG_TERMINATE_BACKEND\",\"PG_TOTAL_RELATION_SIZE\",\"PG_TRY_ADVISORY_LOCK\",\"PG_TRY_ADVISORY_LOCK_SHARED\",\"PG_TRY_ADVISORY_XACT_LOCK\",\"PG_TRY_ADVISORY_XACT_LOCK_SHARED\",\"PG_WALFILE_NAME\",\"PG_WALFILE_NAME_OFFSET\",\"PG_WAL_LSN_DIFF\",\"PG_WAL_REPLAY_PAUSE\",\"PG_WAL_REPLAY_RESUME\",\"SET_CONFIG\",\"SUPPRESS_REDUNDANT_UPDATES_TRIGGER\",\"TSVECTOR_UPDATE_TRIGGER\",\"TSVECTOR_UPDATE_TRIGGER_COLUMN\",\"PG_EVENT_TRIGGER_DDL_COMMANDS\",\"PG_EVENT_TRIGGER_DROPPED_OBJECTS\",\"PG_EVENT_TRIGGER_TABLE_REWRITE_OID\",\"PG_EVENT_TRIGGER_TABLE_REWRITE_REASON\",\"PG_GET_OBJECT_ADDRESS\",\"PG_MCV_LIST_ITEMS\",\"CAST\"],kS=[\"ALL\",\"ANALYSE\",\"ANALYZE\",\"AND\",\"ANY\",\"AS\",\"ASC\",\"ASYMMETRIC\",\"AUTHORIZATION\",\"BETWEEN\",\"BINARY\",\"BOTH\",\"CASE\",\"CAST\",\"CHECK\",\"COLLATE\",\"COLLATION\",\"COLUMN\",\"CONCURRENTLY\",\"CONSTRAINT\",\"CREATE\",\"CROSS\",\"CURRENT_CATALOG\",\"CURRENT_DATE\",\"CURRENT_ROLE\",\"CURRENT_SCHEMA\",\"CURRENT_TIME\",\"CURRENT_TIMESTAMP\",\"CURRENT_USER\",\"DAY\",\"DEFAULT\",\"DEFERRABLE\",\"DESC\",\"DISTINCT\",\"DO\",\"ELSE\",\"END\",\"EXCEPT\",\"EXISTS\",\"FALSE\",\"FETCH\",\"FILTER\",\"FOR\",\"FOREIGN\",\"FREEZE\",\"FROM\",\"FULL\",\"GRANT\",\"GROUP\",\"HAVING\",\"HOUR\",\"ILIKE\",\"IN\",\"INITIALLY\",\"INNER\",\"INOUT\",\"INTERSECT\",\"INTO\",\"IS\",\"ISNULL\",\"JOIN\",\"LATERAL\",\"LEADING\",\"LEFT\",\"LIKE\",\"LIMIT\",\"LOCALTIME\",\"LOCALTIMESTAMP\",\"MINUTE\",\"MONTH\",\"NATURAL\",\"NOT\",\"NOTNULL\",\"NULL\",\"NULLIF\",\"OFFSET\",\"ON\",\"ONLY\",\"OR\",\"ORDER\",\"OUT\",\"OUTER\",\"OVER\",\"OVERLAPS\",\"PLACING\",\"PRIMARY\",\"REFERENCES\",\"RETURNING\",\"RIGHT\",\"ROW\",\"SECOND\",\"SELECT\",\"SESSION_USER\",\"SIMILAR\",\"SOME\",\"SYMMETRIC\",\"TABLE\",\"TABLESAMPLE\",\"THEN\",\"TO\",\"TRAILING\",\"TRUE\",\"UNION\",\"UNIQUE\",\"USER\",\"USING\",\"VALUES\",\"VARIADIC\",\"VERBOSE\",\"WHEN\",\"WHERE\",\"WINDOW\",\"WITH\",\"WITHIN\",\"WITHOUT\",\"YEAR\"],KS=[\"ARRAY\",\"BIGINT\",\"BIT\",\"BIT VARYING\",\"BOOL\",\"BOOLEAN\",\"CHAR\",\"CHARACTER\",\"CHARACTER VARYING\",\"DECIMAL\",\"DEC\",\"DOUBLE\",\"ENUM\",\"FLOAT\",\"INT\",\"INTEGER\",\"INTERVAL\",\"NCHAR\",\"NUMERIC\",\"PRECISION\",\"REAL\",\"SMALLINT\",\"TEXT\",\"TIME\",\"TIMESTAMP\",\"TIMESTAMPTZ\",\"VARCHAR\",\"XML\",\"ZONE\"],JS=v([\"SELECT [ALL | DISTINCT]\"]),qS=v([\"WITH [RECURSIVE]\",\"FROM\",\"WHERE\",\"GROUP BY [ALL | DISTINCT]\",\"HAVING\",\"WINDOW\",\"PARTITION BY\",\"ORDER BY\",\"LIMIT\",\"OFFSET\",\"FETCH {FIRST | NEXT}\",\"FOR {UPDATE | NO KEY UPDATE | SHARE | KEY SHARE} [OF]\",\"INSERT INTO\",\"VALUES\",\"DEFAULT VALUES\",\"SET\",\"RETURNING\"]),ir=v([\"CREATE [GLOBAL | LOCAL] [TEMPORARY | TEMP | UNLOGGED] TABLE [IF NOT EXISTS]\"]),jt=v([\"CREATE [OR REPLACE] [TEMP | TEMPORARY] [RECURSIVE] VIEW\",\"CREATE [MATERIALIZED] VIEW [IF NOT EXISTS]\",\"UPDATE [ONLY]\",\"WHERE CURRENT OF\",\"ON CONFLICT\",\"DELETE FROM [ONLY]\",\"DROP TABLE [IF EXISTS]\",\"ALTER TABLE [IF EXISTS] [ONLY]\",\"ALTER TABLE ALL IN TABLESPACE\",\"RENAME [COLUMN]\",\"RENAME TO\",\"ADD [COLUMN] [IF NOT EXISTS]\",\"DROP [COLUMN] [IF EXISTS]\",\"ALTER [COLUMN]\",\"SET DATA TYPE\",\"{SET | DROP} DEFAULT\",\"{SET | DROP} NOT NULL\",\"TRUNCATE [TABLE] [ONLY]\",\"SET SCHEMA\",\"AFTER\",\"ABORT\",\"ALTER AGGREGATE\",\"ALTER COLLATION\",\"ALTER CONVERSION\",\"ALTER DATABASE\",\"ALTER DEFAULT PRIVILEGES\",\"ALTER DOMAIN\",\"ALTER EVENT TRIGGER\",\"ALTER EXTENSION\",\"ALTER FOREIGN DATA WRAPPER\",\"ALTER FOREIGN TABLE\",\"ALTER FUNCTION\",\"ALTER GROUP\",\"ALTER INDEX\",\"ALTER LANGUAGE\",\"ALTER LARGE OBJECT\",\"ALTER MATERIALIZED VIEW\",\"ALTER OPERATOR\",\"ALTER OPERATOR CLASS\",\"ALTER OPERATOR FAMILY\",\"ALTER POLICY\",\"ALTER PROCEDURE\",\"ALTER PUBLICATION\",\"ALTER ROLE\",\"ALTER ROUTINE\",\"ALTER RULE\",\"ALTER SCHEMA\",\"ALTER SEQUENCE\",\"ALTER SERVER\",\"ALTER STATISTICS\",\"ALTER SUBSCRIPTION\",\"ALTER SYSTEM\",\"ALTER TABLESPACE\",\"ALTER TEXT SEARCH CONFIGURATION\",\"ALTER TEXT SEARCH DICTIONARY\",\"ALTER TEXT SEARCH PARSER\",\"ALTER TEXT SEARCH TEMPLATE\",\"ALTER TRIGGER\",\"ALTER TYPE\",\"ALTER USER\",\"ALTER USER MAPPING\",\"ALTER VIEW\",\"ANALYZE\",\"BEGIN\",\"CALL\",\"CHECKPOINT\",\"CLOSE\",\"CLUSTER\",\"COMMIT\",\"COMMIT PREPARED\",\"COPY\",\"CREATE ACCESS METHOD\",\"CREATE AGGREGATE\",\"CREATE CAST\",\"CREATE COLLATION\",\"CREATE CONVERSION\",\"CREATE DATABASE\",\"CREATE DOMAIN\",\"CREATE EVENT TRIGGER\",\"CREATE EXTENSION\",\"CREATE FOREIGN DATA WRAPPER\",\"CREATE FOREIGN TABLE\",\"CREATE FUNCTION\",\"CREATE GROUP\",\"CREATE INDEX\",\"CREATE LANGUAGE\",\"CREATE OPERATOR\",\"CREATE OPERATOR CLASS\",\"CREATE OPERATOR FAMILY\",\"CREATE POLICY\",\"CREATE PROCEDURE\",\"CREATE PUBLICATION\",\"CREATE ROLE\",\"CREATE RULE\",\"CREATE SCHEMA\",\"CREATE SEQUENCE\",\"CREATE SERVER\",\"CREATE STATISTICS\",\"CREATE SUBSCRIPTION\",\"CREATE TABLESPACE\",\"CREATE TEXT SEARCH CONFIGURATION\",\"CREATE TEXT SEARCH DICTIONARY\",\"CREATE TEXT SEARCH PARSER\",\"CREATE TEXT SEARCH TEMPLATE\",\"CREATE TRANSFORM\",\"CREATE TRIGGER\",\"CREATE TYPE\",\"CREATE USER\",\"CREATE USER MAPPING\",\"DEALLOCATE\",\"DECLARE\",\"DISCARD\",\"DROP ACCESS METHOD\",\"DROP AGGREGATE\",\"DROP CAST\",\"DROP COLLATION\",\"DROP CONVERSION\",\"DROP DATABASE\",\"DROP DOMAIN\",\"DROP EVENT TRIGGER\",\"DROP EXTENSION\",\"DROP FOREIGN DATA WRAPPER\",\"DROP FOREIGN TABLE\",\"DROP FUNCTION\",\"DROP GROUP\",\"DROP INDEX\",\"DROP LANGUAGE\",\"DROP MATERIALIZED VIEW\",\"DROP OPERATOR\",\"DROP OPERATOR CLASS\",\"DROP OPERATOR FAMILY\",\"DROP OWNED\",\"DROP POLICY\",\"DROP PROCEDURE\",\"DROP PUBLICATION\",\"DROP ROLE\",\"DROP ROUTINE\",\"DROP RULE\",\"DROP SCHEMA\",\"DROP SEQUENCE\",\"DROP SERVER\",\"DROP STATISTICS\",\"DROP SUBSCRIPTION\",\"DROP TABLESPACE\",\"DROP TEXT SEARCH CONFIGURATION\",\"DROP TEXT SEARCH DICTIONARY\",\"DROP TEXT SEARCH PARSER\",\"DROP TEXT SEARCH TEMPLATE\",\"DROP TRANSFORM\",\"DROP TRIGGER\",\"DROP TYPE\",\"DROP USER\",\"DROP USER MAPPING\",\"DROP VIEW\",\"EXECUTE\",\"EXPLAIN\",\"FETCH\",\"GRANT\",\"IMPORT FOREIGN SCHEMA\",\"LISTEN\",\"LOAD\",\"LOCK\",\"MOVE\",\"NOTIFY\",\"PREPARE\",\"PREPARE TRANSACTION\",\"REASSIGN OWNED\",\"REFRESH MATERIALIZED VIEW\",\"REINDEX\",\"RELEASE SAVEPOINT\",\"RESET\",\"REVOKE\",\"ROLLBACK\",\"ROLLBACK PREPARED\",\"ROLLBACK TO SAVEPOINT\",\"SAVEPOINT\",\"SECURITY LABEL\",\"SELECT INTO\",\"SET CONSTRAINTS\",\"SET ROLE\",\"SET SESSION AUTHORIZATION\",\"SET TRANSACTION\",\"SHOW\",\"START TRANSACTION\",\"UNLISTEN\",\"VACUUM\"]),QS=v([\"UNION [ALL | DISTINCT]\",\"EXCEPT [ALL | DISTINCT]\",\"INTERSECT [ALL | DISTINCT]\"]),ZS=v([\"JOIN\",\"{LEFT | RIGHT | FULL} [OUTER] JOIN\",\"{INNER | CROSS} JOIN\",\"NATURAL [INNER] JOIN\",\"NATURAL {LEFT | RIGHT | FULL} [OUTER] JOIN\"]),jS=v([\"PRIMARY KEY\",\"GENERATED {ALWAYS | BY DEFAULT} AS IDENTITY\",\"ON {UPDATE | DELETE} [SET NULL | SET DEFAULT]\",\"{ROWS | RANGE | GROUPS} BETWEEN\",\"[TIMESTAMP | TIME] {WITH | WITHOUT} TIME ZONE\",\"IS [NOT] DISTINCT FROM\"]),zS={name:\"postgresql\",tokenizerOptions:{reservedSelect:JS,reservedClauses:[...qS,...ir,...jt],reservedSetOperations:QS,reservedJoins:ZS,reservedPhrases:jS,reservedKeywords:kS,reservedDataTypes:KS,reservedFunctionNames:XS,nestedBlockComments:!0,extraParens:[\"[]\"],stringTypes:[\"$$\",{quote:\"''-qq\",prefixes:[\"U&\"]},{quote:\"''-qq-bs\",prefixes:[\"E\"],requirePrefix:!0},{quote:\"''-raw\",prefixes:[\"B\",\"X\"],requirePrefix:!0}],identTypes:[{quote:'\"\"-qq',prefixes:[\"U&\"]}],identChars:{rest:\"$\"},paramTypes:{numbered:[\"$\"]},operators:[\"%\",\"^\",\"|/\",\"||/\",\"@\",\":=\",\"&\",\"|\",\"#\",\"~\",\"<<\",\">>\",\"~>~\",\"~<~\",\"~>=~\",\"~<=~\",\"@-@\",\"@@\",\"##\",\"<->\",\"&&\",\"&<\",\"&>\",\"<<|\",\"&<|\",\"|>>\",\"|&>\",\"<^\",\"^>\",\"?#\",\"?-\",\"?|\",\"?-|\",\"?||\",\"@>\",\"<@\",\"~=\",\"?\",\"@?\",\"?&\",\"->\",\"->>\",\"#>\",\"#>>\",\"#-\",\"=>\",\">>=\",\"<<=\",\"~~\",\"~~*\",\"!~~\",\"!~~*\",\"~\",\"~*\",\"!~\",\"!~*\",\"-|-\",\"||\",\"@@@\",\"!!\",\"^@\",\"<%\",\"%>\",\"<<%\",\"%>>\",\"<<->\",\"<->>\",\"<<<->\",\"<->>>\",\"::\",\":\"]},formatOptions:{alwaysDenseOperators:[\"::\",\":\"],onelineClauses:[...ir,...jt],tabularOnelineClauses:jt}},eo=[\"ANY_VALUE\",\"APPROXIMATE PERCENTILE_DISC\",\"AVG\",\"COUNT\",\"LISTAGG\",\"MAX\",\"MEDIAN\",\"MIN\",\"PERCENTILE_CONT\",\"STDDEV_SAMP\",\"STDDEV_POP\",\"SUM\",\"VAR_SAMP\",\"VAR_POP\",\"array\",\"array_concat\",\"array_flatten\",\"get_array_length\",\"split_to_array\",\"subarray\",\"BIT_AND\",\"BIT_OR\",\"BOOL_AND\",\"BOOL_OR\",\"COALESCE\",\"DECODE\",\"GREATEST\",\"LEAST\",\"NVL\",\"NVL2\",\"NULLIF\",\"ADD_MONTHS\",\"AT TIME ZONE\",\"CONVERT_TIMEZONE\",\"CURRENT_DATE\",\"CURRENT_TIME\",\"CURRENT_TIMESTAMP\",\"DATE_CMP\",\"DATE_CMP_TIMESTAMP\",\"DATE_CMP_TIMESTAMPTZ\",\"DATE_PART_YEAR\",\"DATEADD\",\"DATEDIFF\",\"DATE_PART\",\"DATE_TRUNC\",\"EXTRACT\",\"GETDATE\",\"INTERVAL_CMP\",\"LAST_DAY\",\"MONTHS_BETWEEN\",\"NEXT_DAY\",\"SYSDATE\",\"TIMEOFDAY\",\"TIMESTAMP_CMP\",\"TIMESTAMP_CMP_DATE\",\"TIMESTAMP_CMP_TIMESTAMPTZ\",\"TIMESTAMPTZ_CMP\",\"TIMESTAMPTZ_CMP_DATE\",\"TIMESTAMPTZ_CMP_TIMESTAMP\",\"TIMEZONE\",\"TO_TIMESTAMP\",\"TRUNC\",\"AddBBox\",\"DropBBox\",\"GeometryType\",\"ST_AddPoint\",\"ST_Angle\",\"ST_Area\",\"ST_AsBinary\",\"ST_AsEWKB\",\"ST_AsEWKT\",\"ST_AsGeoJSON\",\"ST_AsText\",\"ST_Azimuth\",\"ST_Boundary\",\"ST_Collect\",\"ST_Contains\",\"ST_ContainsProperly\",\"ST_ConvexHull\",\"ST_CoveredBy\",\"ST_Covers\",\"ST_Crosses\",\"ST_Dimension\",\"ST_Disjoint\",\"ST_Distance\",\"ST_DistanceSphere\",\"ST_DWithin\",\"ST_EndPoint\",\"ST_Envelope\",\"ST_Equals\",\"ST_ExteriorRing\",\"ST_Force2D\",\"ST_Force3D\",\"ST_Force3DM\",\"ST_Force3DZ\",\"ST_Force4D\",\"ST_GeometryN\",\"ST_GeometryType\",\"ST_GeomFromEWKB\",\"ST_GeomFromEWKT\",\"ST_GeomFromText\",\"ST_GeomFromWKB\",\"ST_InteriorRingN\",\"ST_Intersects\",\"ST_IsPolygonCCW\",\"ST_IsPolygonCW\",\"ST_IsClosed\",\"ST_IsCollection\",\"ST_IsEmpty\",\"ST_IsSimple\",\"ST_IsValid\",\"ST_Length\",\"ST_LengthSphere\",\"ST_Length2D\",\"ST_LineFromMultiPoint\",\"ST_LineInterpolatePoint\",\"ST_M\",\"ST_MakeEnvelope\",\"ST_MakeLine\",\"ST_MakePoint\",\"ST_MakePolygon\",\"ST_MemSize\",\"ST_MMax\",\"ST_MMin\",\"ST_Multi\",\"ST_NDims\",\"ST_NPoints\",\"ST_NRings\",\"ST_NumGeometries\",\"ST_NumInteriorRings\",\"ST_NumPoints\",\"ST_Perimeter\",\"ST_Perimeter2D\",\"ST_Point\",\"ST_PointN\",\"ST_Points\",\"ST_Polygon\",\"ST_RemovePoint\",\"ST_Reverse\",\"ST_SetPoint\",\"ST_SetSRID\",\"ST_Simplify\",\"ST_SRID\",\"ST_StartPoint\",\"ST_Touches\",\"ST_Within\",\"ST_X\",\"ST_XMax\",\"ST_XMin\",\"ST_Y\",\"ST_YMax\",\"ST_YMin\",\"ST_Z\",\"ST_ZMax\",\"ST_ZMin\",\"SupportsBBox\",\"CHECKSUM\",\"FUNC_SHA1\",\"FNV_HASH\",\"MD5\",\"SHA\",\"SHA1\",\"SHA2\",\"HLL\",\"HLL_CREATE_SKETCH\",\"HLL_CARDINALITY\",\"HLL_COMBINE\",\"IS_VALID_JSON\",\"IS_VALID_JSON_ARRAY\",\"JSON_ARRAY_LENGTH\",\"JSON_EXTRACT_ARRAY_ELEMENT_TEXT\",\"JSON_EXTRACT_PATH_TEXT\",\"JSON_PARSE\",\"JSON_SERIALIZE\",\"ABS\",\"ACOS\",\"ASIN\",\"ATAN\",\"ATAN2\",\"CBRT\",\"CEILING\",\"CEIL\",\"COS\",\"COT\",\"DEGREES\",\"DEXP\",\"DLOG1\",\"DLOG10\",\"EXP\",\"FLOOR\",\"LN\",\"LOG\",\"MOD\",\"PI\",\"POWER\",\"RADIANS\",\"RANDOM\",\"ROUND\",\"SIN\",\"SIGN\",\"SQRT\",\"TAN\",\"TO_HEX\",\"TRUNC\",\"EXPLAIN_MODEL\",\"ASCII\",\"BPCHARCMP\",\"BTRIM\",\"BTTEXT_PATTERN_CMP\",\"CHAR_LENGTH\",\"CHARACTER_LENGTH\",\"CHARINDEX\",\"CHR\",\"COLLATE\",\"CONCAT\",\"CRC32\",\"DIFFERENCE\",\"INITCAP\",\"LEFT\",\"RIGHT\",\"LEN\",\"LENGTH\",\"LOWER\",\"LPAD\",\"RPAD\",\"LTRIM\",\"OCTETINDEX\",\"OCTET_LENGTH\",\"POSITION\",\"QUOTE_IDENT\",\"QUOTE_LITERAL\",\"REGEXP_COUNT\",\"REGEXP_INSTR\",\"REGEXP_REPLACE\",\"REGEXP_SUBSTR\",\"REPEAT\",\"REPLACE\",\"REPLICATE\",\"REVERSE\",\"RTRIM\",\"SOUNDEX\",\"SPLIT_PART\",\"STRPOS\",\"STRTOL\",\"SUBSTRING\",\"TEXTLEN\",\"TRANSLATE\",\"TRIM\",\"UPPER\",\"decimal_precision\",\"decimal_scale\",\"is_array\",\"is_bigint\",\"is_boolean\",\"is_char\",\"is_decimal\",\"is_float\",\"is_integer\",\"is_object\",\"is_scalar\",\"is_smallint\",\"is_varchar\",\"json_typeof\",\"AVG\",\"COUNT\",\"CUME_DIST\",\"DENSE_RANK\",\"FIRST_VALUE\",\"LAST_VALUE\",\"LAG\",\"LEAD\",\"LISTAGG\",\"MAX\",\"MEDIAN\",\"MIN\",\"NTH_VALUE\",\"NTILE\",\"PERCENT_RANK\",\"PERCENTILE_CONT\",\"PERCENTILE_DISC\",\"RANK\",\"RATIO_TO_REPORT\",\"ROW_NUMBER\",\"STDDEV_SAMP\",\"STDDEV_POP\",\"SUM\",\"VAR_SAMP\",\"VAR_POP\",\"CAST\",\"CONVERT\",\"TO_CHAR\",\"TO_DATE\",\"TO_NUMBER\",\"TEXT_TO_INT_ALT\",\"TEXT_TO_NUMERIC_ALT\",\"CHANGE_QUERY_PRIORITY\",\"CHANGE_SESSION_PRIORITY\",\"CHANGE_USER_PRIORITY\",\"CURRENT_SETTING\",\"PG_CANCEL_BACKEND\",\"PG_TERMINATE_BACKEND\",\"REBOOT_CLUSTER\",\"SET_CONFIG\",\"CURRENT_AWS_ACCOUNT\",\"CURRENT_DATABASE\",\"CURRENT_NAMESPACE\",\"CURRENT_SCHEMA\",\"CURRENT_SCHEMAS\",\"CURRENT_USER\",\"CURRENT_USER_ID\",\"HAS_ASSUMEROLE_PRIVILEGE\",\"HAS_DATABASE_PRIVILEGE\",\"HAS_SCHEMA_PRIVILEGE\",\"HAS_TABLE_PRIVILEGE\",\"PG_BACKEND_PID\",\"PG_GET_COLS\",\"PG_GET_GRANTEE_BY_IAM_ROLE\",\"PG_GET_IAM_ROLE_BY_USER\",\"PG_GET_LATE_BINDING_VIEW_COLS\",\"PG_LAST_COPY_COUNT\",\"PG_LAST_COPY_ID\",\"PG_LAST_UNLOAD_ID\",\"PG_LAST_QUERY_ID\",\"PG_LAST_UNLOAD_COUNT\",\"SESSION_USER\",\"SLICE_NUM\",\"USER\",\"VERSION\"],Eo=[\"AES128\",\"AES256\",\"ALL\",\"ALLOWOVERWRITE\",\"ANY\",\"AS\",\"ASC\",\"AUTHORIZATION\",\"BACKUP\",\"BETWEEN\",\"BINARY\",\"BOTH\",\"CHECK\",\"COLUMN\",\"CONSTRAINT\",\"CREATE\",\"CROSS\",\"DEFAULT\",\"DEFERRABLE\",\"DEFLATE\",\"DEFRAG\",\"DESC\",\"DISABLE\",\"DISTINCT\",\"DO\",\"ENABLE\",\"ENCODE\",\"ENCRYPT\",\"ENCRYPTION\",\"EXPLICIT\",\"FALSE\",\"FOR\",\"FOREIGN\",\"FREEZE\",\"FROM\",\"FULL\",\"GLOBALDICT256\",\"GLOBALDICT64K\",\"GROUP\",\"IDENTITY\",\"IGNORE\",\"ILIKE\",\"IN\",\"INITIALLY\",\"INNER\",\"INTO\",\"IS\",\"ISNULL\",\"LANGUAGE\",\"LEADING\",\"LIKE\",\"LIMIT\",\"LOCALTIME\",\"LOCALTIMESTAMP\",\"LUN\",\"LUNS\",\"MINUS\",\"NATURAL\",\"NEW\",\"NOT\",\"NOTNULL\",\"NULL\",\"NULLS\",\"OFF\",\"OFFLINE\",\"OFFSET\",\"OID\",\"OLD\",\"ON\",\"ONLY\",\"OPEN\",\"ORDER\",\"OUTER\",\"OVERLAPS\",\"PARALLEL\",\"PARTITION\",\"PERCENT\",\"PERMISSIONS\",\"PLACING\",\"PRIMARY\",\"RECOVER\",\"REFERENCES\",\"REJECTLOG\",\"RESORT\",\"RESPECT\",\"RESTORE\",\"SIMILAR\",\"SNAPSHOT\",\"SOME\",\"SYSTEM\",\"TABLE\",\"TAG\",\"TDES\",\"THEN\",\"TIMESTAMP\",\"TO\",\"TOP\",\"TRAILING\",\"TRUE\",\"UNIQUE\",\"USING\",\"VERBOSE\",\"WALLET\",\"WITHOUT\",\"ACCEPTANYDATE\",\"ACCEPTINVCHARS\",\"BLANKSASNULL\",\"DATEFORMAT\",\"EMPTYASNULL\",\"ENCODING\",\"ESCAPE\",\"EXPLICIT_IDS\",\"FILLRECORD\",\"IGNOREBLANKLINES\",\"IGNOREHEADER\",\"REMOVEQUOTES\",\"ROUNDEC\",\"TIMEFORMAT\",\"TRIMBLANKS\",\"TRUNCATECOLUMNS\",\"COMPROWS\",\"COMPUPDATE\",\"MAXERROR\",\"NOLOAD\",\"STATUPDATE\",\"FORMAT\",\"CSV\",\"DELIMITER\",\"FIXEDWIDTH\",\"SHAPEFILE\",\"AVRO\",\"JSON\",\"PARQUET\",\"ORC\",\"ACCESS_KEY_ID\",\"CREDENTIALS\",\"ENCRYPTED\",\"IAM_ROLE\",\"MASTER_SYMMETRIC_KEY\",\"SECRET_ACCESS_KEY\",\"SESSION_TOKEN\",\"BZIP2\",\"GZIP\",\"LZOP\",\"ZSTD\",\"MANIFEST\",\"READRATIO\",\"REGION\",\"SSH\",\"RAW\",\"AZ64\",\"BYTEDICT\",\"DELTA\",\"DELTA32K\",\"LZO\",\"MOSTLY8\",\"MOSTLY16\",\"MOSTLY32\",\"RUNLENGTH\",\"TEXT255\",\"TEXT32K\",\"CATALOG_ROLE\",\"SECRET_ARN\",\"EXTERNAL\",\"AUTO\",\"EVEN\",\"KEY\",\"PREDICATE\",\"COMPRESSION\"],to=[\"ARRAY\",\"BIGINT\",\"BPCHAR\",\"CHAR\",\"CHARACTER VARYING\",\"CHARACTER\",\"DECIMAL\",\"INT\",\"INT2\",\"INT4\",\"INT8\",\"INTEGER\",\"NCHAR\",\"NUMERIC\",\"NVARCHAR\",\"SMALLINT\",\"TEXT\",\"VARBYTE\",\"VARCHAR\"],To=v([\"SELECT [ALL | DISTINCT]\"]),ro=v([\"WITH [RECURSIVE]\",\"FROM\",\"WHERE\",\"GROUP BY\",\"HAVING\",\"PARTITION BY\",\"ORDER BY\",\"LIMIT\",\"OFFSET\",\"INSERT INTO\",\"VALUES\",\"SET\"]),ar=v([\"CREATE [TEMPORARY | TEMP | LOCAL TEMPORARY | LOCAL TEMP] TABLE [IF NOT EXISTS]\"]),zt=v([\"CREATE [OR REPLACE | MATERIALIZED] VIEW\",\"UPDATE\",\"DELETE [FROM]\",\"DROP TABLE [IF EXISTS]\",\"ALTER TABLE\",\"ALTER TABLE APPEND\",\"ADD [COLUMN]\",\"DROP [COLUMN]\",\"RENAME TO\",\"RENAME COLUMN\",\"ALTER COLUMN\",\"TYPE\",\"ENCODE\",\"TRUNCATE [TABLE]\",\"ABORT\",\"ALTER DATABASE\",\"ALTER DATASHARE\",\"ALTER DEFAULT PRIVILEGES\",\"ALTER GROUP\",\"ALTER MATERIALIZED VIEW\",\"ALTER PROCEDURE\",\"ALTER SCHEMA\",\"ALTER USER\",\"ANALYSE\",\"ANALYZE\",\"ANALYSE COMPRESSION\",\"ANALYZE COMPRESSION\",\"BEGIN\",\"CALL\",\"CANCEL\",\"CLOSE\",\"COMMIT\",\"COPY\",\"CREATE DATABASE\",\"CREATE DATASHARE\",\"CREATE EXTERNAL FUNCTION\",\"CREATE EXTERNAL SCHEMA\",\"CREATE EXTERNAL TABLE\",\"CREATE FUNCTION\",\"CREATE GROUP\",\"CREATE LIBRARY\",\"CREATE MODEL\",\"CREATE PROCEDURE\",\"CREATE SCHEMA\",\"CREATE USER\",\"DEALLOCATE\",\"DECLARE\",\"DESC DATASHARE\",\"DROP DATABASE\",\"DROP DATASHARE\",\"DROP FUNCTION\",\"DROP GROUP\",\"DROP LIBRARY\",\"DROP MODEL\",\"DROP MATERIALIZED VIEW\",\"DROP PROCEDURE\",\"DROP SCHEMA\",\"DROP USER\",\"DROP VIEW\",\"DROP\",\"EXECUTE\",\"EXPLAIN\",\"FETCH\",\"GRANT\",\"LOCK\",\"PREPARE\",\"REFRESH MATERIALIZED VIEW\",\"RESET\",\"REVOKE\",\"ROLLBACK\",\"SELECT INTO\",\"SET SESSION AUTHORIZATION\",\"SET SESSION CHARACTERISTICS\",\"SHOW\",\"SHOW EXTERNAL TABLE\",\"SHOW MODEL\",\"SHOW DATASHARES\",\"SHOW PROCEDURE\",\"SHOW TABLE\",\"SHOW VIEW\",\"START TRANSACTION\",\"UNLOAD\",\"VACUUM\"]),Ro=v([\"UNION [ALL]\",\"EXCEPT\",\"INTERSECT\",\"MINUS\"]),no=v([\"JOIN\",\"{LEFT | RIGHT | FULL} [OUTER] JOIN\",\"{INNER | CROSS} JOIN\",\"NATURAL [INNER] JOIN\",\"NATURAL {LEFT | RIGHT | FULL} [OUTER] JOIN\"]),Ao=v([\"NULL AS\",\"DATA CATALOG\",\"HIVE METASTORE\",\"{ROWS | RANGE} BETWEEN\"]),so={name:\"redshift\",tokenizerOptions:{reservedSelect:To,reservedClauses:[...ro,...ar,...zt],reservedSetOperations:Ro,reservedJoins:no,reservedPhrases:Ao,reservedKeywords:Eo,reservedDataTypes:to,reservedFunctionNames:eo,stringTypes:[\"''-qq\"],identTypes:['\"\"-qq'],identChars:{first:\"#\"},paramTypes:{numbered:[\"$\"]},operators:[\"^\",\"%\",\"@\",\"|/\",\"||/\",\"&\",\"|\",\"~\",\"<<\",\">>\",\"||\",\"::\"]},formatOptions:{alwaysDenseOperators:[\"::\"],onelineClauses:[...ar,...zt],tabularOnelineClauses:zt}},So=[\"ADD\",\"AFTER\",\"ALL\",\"ALTER\",\"ANALYZE\",\"AND\",\"ANTI\",\"ANY\",\"ARCHIVE\",\"AS\",\"ASC\",\"AT\",\"AUTHORIZATION\",\"BETWEEN\",\"BOTH\",\"BUCKET\",\"BUCKETS\",\"BY\",\"CACHE\",\"CASCADE\",\"CAST\",\"CHANGE\",\"CHECK\",\"CLEAR\",\"CLUSTER\",\"CLUSTERED\",\"CODEGEN\",\"COLLATE\",\"COLLECTION\",\"COLUMN\",\"COLUMNS\",\"COMMENT\",\"COMMIT\",\"COMPACT\",\"COMPACTIONS\",\"COMPUTE\",\"CONCATENATE\",\"CONSTRAINT\",\"COST\",\"CREATE\",\"CROSS\",\"CUBE\",\"CURRENT\",\"CURRENT_DATE\",\"CURRENT_TIME\",\"CURRENT_TIMESTAMP\",\"CURRENT_USER\",\"DATA\",\"DATABASE\",\"DATABASES\",\"DAY\",\"DBPROPERTIES\",\"DEFINED\",\"DELETE\",\"DELIMITED\",\"DESC\",\"DESCRIBE\",\"DFS\",\"DIRECTORIES\",\"DIRECTORY\",\"DISTINCT\",\"DISTRIBUTE\",\"DIV\",\"DROP\",\"ESCAPE\",\"ESCAPED\",\"EXCEPT\",\"EXCHANGE\",\"EXISTS\",\"EXPORT\",\"EXTENDED\",\"EXTERNAL\",\"EXTRACT\",\"FALSE\",\"FETCH\",\"FIELDS\",\"FILTER\",\"FILEFORMAT\",\"FIRST\",\"FIRST_VALUE\",\"FOLLOWING\",\"FOR\",\"FOREIGN\",\"FORMAT\",\"FORMATTED\",\"FULL\",\"FUNCTION\",\"FUNCTIONS\",\"GLOBAL\",\"GRANT\",\"GROUP\",\"GROUPING\",\"HOUR\",\"IF\",\"IGNORE\",\"IMPORT\",\"IN\",\"INDEX\",\"INDEXES\",\"INNER\",\"INPATH\",\"INPUTFORMAT\",\"INTERSECT\",\"INTO\",\"IS\",\"ITEMS\",\"KEYS\",\"LAST\",\"LAST_VALUE\",\"LATERAL\",\"LAZY\",\"LEADING\",\"LEFT\",\"LIKE\",\"LINES\",\"LIST\",\"LOCAL\",\"LOCATION\",\"LOCK\",\"LOCKS\",\"LOGICAL\",\"MACRO\",\"MATCHED\",\"MERGE\",\"MINUTE\",\"MONTH\",\"MSCK\",\"NAMESPACE\",\"NAMESPACES\",\"NATURAL\",\"NO\",\"NOT\",\"NULL\",\"NULLS\",\"OF\",\"ONLY\",\"OPTION\",\"OPTIONS\",\"OR\",\"ORDER\",\"OUT\",\"OUTER\",\"OUTPUTFORMAT\",\"OVER\",\"OVERLAPS\",\"OVERLAY\",\"OVERWRITE\",\"OWNER\",\"PARTITION\",\"PARTITIONED\",\"PARTITIONS\",\"PERCENT\",\"PLACING\",\"POSITION\",\"PRECEDING\",\"PRIMARY\",\"PRINCIPALS\",\"PROPERTIES\",\"PURGE\",\"QUERY\",\"RANGE\",\"RECORDREADER\",\"RECORDWRITER\",\"RECOVER\",\"REDUCE\",\"REFERENCES\",\"RENAME\",\"REPAIR\",\"REPLACE\",\"RESPECT\",\"RESTRICT\",\"REVOKE\",\"RIGHT\",\"RLIKE\",\"ROLE\",\"ROLES\",\"ROLLBACK\",\"ROLLUP\",\"ROW\",\"ROWS\",\"SCHEMA\",\"SECOND\",\"SELECT\",\"SEMI\",\"SEPARATED\",\"SERDE\",\"SERDEPROPERTIES\",\"SESSION_USER\",\"SETS\",\"SHOW\",\"SKEWED\",\"SOME\",\"SORT\",\"SORTED\",\"START\",\"STATISTICS\",\"STORED\",\"STRATIFY\",\"SUBSTR\",\"SUBSTRING\",\"TABLE\",\"TABLES\",\"TBLPROPERTIES\",\"TEMPORARY\",\"TERMINATED\",\"THEN\",\"TO\",\"TOUCH\",\"TRAILING\",\"TRANSACTION\",\"TRANSACTIONS\",\"TRIM\",\"TRUE\",\"TRUNCATE\",\"UNARCHIVE\",\"UNBOUNDED\",\"UNCACHE\",\"UNIQUE\",\"UNKNOWN\",\"UNLOCK\",\"UNSET\",\"USE\",\"USER\",\"USING\",\"VIEW\",\"WINDOW\",\"YEAR\",\"ANALYSE\",\"ARRAY_ZIP\",\"COALESCE\",\"CONTAINS\",\"CONVERT\",\"DAYS\",\"DAY_HOUR\",\"DAY_MINUTE\",\"DAY_SECOND\",\"DECODE\",\"DEFAULT\",\"DISTINCTROW\",\"ENCODE\",\"EXPLODE\",\"EXPLODE_OUTER\",\"FIXED\",\"GREATEST\",\"GROUP_CONCAT\",\"HOURS\",\"HOUR_MINUTE\",\"HOUR_SECOND\",\"IFNULL\",\"LEAST\",\"LEVEL\",\"MINUTE_SECOND\",\"NULLIF\",\"OFFSET\",\"ON\",\"OPTIMIZE\",\"REGEXP\",\"SEPARATOR\",\"SIZE\",\"TYPE\",\"TYPES\",\"UNSIGNED\",\"VARIABLES\",\"YEAR_MONTH\"],oo=[\"ARRAY\",\"BIGINT\",\"BINARY\",\"BOOLEAN\",\"BYTE\",\"CHAR\",\"DATE\",\"DEC\",\"DECIMAL\",\"DOUBLE\",\"FLOAT\",\"INT\",\"INTEGER\",\"INTERVAL\",\"LONG\",\"MAP\",\"NUMERIC\",\"REAL\",\"SHORT\",\"SMALLINT\",\"STRING\",\"STRUCT\",\"TIMESTAMP_LTZ\",\"TIMESTAMP_NTZ\",\"TIMESTAMP\",\"TINYINT\",\"VARCHAR\"],Oo=[\"APPROX_COUNT_DISTINCT\",\"APPROX_PERCENTILE\",\"AVG\",\"BIT_AND\",\"BIT_OR\",\"BIT_XOR\",\"BOOL_AND\",\"BOOL_OR\",\"COLLECT_LIST\",\"COLLECT_SET\",\"CORR\",\"COUNT\",\"COUNT\",\"COUNT\",\"COUNT_IF\",\"COUNT_MIN_SKETCH\",\"COVAR_POP\",\"COVAR_SAMP\",\"EVERY\",\"FIRST\",\"FIRST_VALUE\",\"GROUPING\",\"GROUPING_ID\",\"KURTOSIS\",\"LAST\",\"LAST_VALUE\",\"MAX\",\"MAX_BY\",\"MEAN\",\"MIN\",\"MIN_BY\",\"PERCENTILE\",\"PERCENTILE\",\"PERCENTILE_APPROX\",\"SKEWNESS\",\"STD\",\"STDDEV\",\"STDDEV_POP\",\"STDDEV_SAMP\",\"SUM\",\"VAR_POP\",\"VAR_SAMP\",\"VARIANCE\",\"CUME_DIST\",\"DENSE_RANK\",\"LAG\",\"LEAD\",\"NTH_VALUE\",\"NTILE\",\"PERCENT_RANK\",\"RANK\",\"ROW_NUMBER\",\"ARRAY\",\"ARRAY_CONTAINS\",\"ARRAY_DISTINCT\",\"ARRAY_EXCEPT\",\"ARRAY_INTERSECT\",\"ARRAY_JOIN\",\"ARRAY_MAX\",\"ARRAY_MIN\",\"ARRAY_POSITION\",\"ARRAY_REMOVE\",\"ARRAY_REPEAT\",\"ARRAY_UNION\",\"ARRAYS_OVERLAP\",\"ARRAYS_ZIP\",\"FLATTEN\",\"SEQUENCE\",\"SHUFFLE\",\"SLICE\",\"SORT_ARRAY\",\"ELEMENT_AT\",\"ELEMENT_AT\",\"MAP_CONCAT\",\"MAP_ENTRIES\",\"MAP_FROM_ARRAYS\",\"MAP_FROM_ENTRIES\",\"MAP_KEYS\",\"MAP_VALUES\",\"STR_TO_MAP\",\"ADD_MONTHS\",\"CURRENT_DATE\",\"CURRENT_DATE\",\"CURRENT_TIMESTAMP\",\"CURRENT_TIMESTAMP\",\"CURRENT_TIMEZONE\",\"DATE_ADD\",\"DATE_FORMAT\",\"DATE_FROM_UNIX_DATE\",\"DATE_PART\",\"DATE_SUB\",\"DATE_TRUNC\",\"DATEDIFF\",\"DAY\",\"DAYOFMONTH\",\"DAYOFWEEK\",\"DAYOFYEAR\",\"EXTRACT\",\"FROM_UNIXTIME\",\"FROM_UTC_TIMESTAMP\",\"HOUR\",\"LAST_DAY\",\"MAKE_DATE\",\"MAKE_DT_INTERVAL\",\"MAKE_INTERVAL\",\"MAKE_TIMESTAMP\",\"MAKE_YM_INTERVAL\",\"MINUTE\",\"MONTH\",\"MONTHS_BETWEEN\",\"NEXT_DAY\",\"NOW\",\"QUARTER\",\"SECOND\",\"SESSION_WINDOW\",\"TIMESTAMP_MICROS\",\"TIMESTAMP_MILLIS\",\"TIMESTAMP_SECONDS\",\"TO_DATE\",\"TO_TIMESTAMP\",\"TO_UNIX_TIMESTAMP\",\"TO_UTC_TIMESTAMP\",\"TRUNC\",\"UNIX_DATE\",\"UNIX_MICROS\",\"UNIX_MILLIS\",\"UNIX_SECONDS\",\"UNIX_TIMESTAMP\",\"WEEKDAY\",\"WEEKOFYEAR\",\"WINDOW\",\"YEAR\",\"FROM_JSON\",\"GET_JSON_OBJECT\",\"JSON_ARRAY_LENGTH\",\"JSON_OBJECT_KEYS\",\"JSON_TUPLE\",\"SCHEMA_OF_JSON\",\"TO_JSON\",\"ABS\",\"ACOS\",\"ACOSH\",\"AGGREGATE\",\"ARRAY_SORT\",\"ASCII\",\"ASIN\",\"ASINH\",\"ASSERT_TRUE\",\"ATAN\",\"ATAN2\",\"ATANH\",\"BASE64\",\"BIN\",\"BIT_COUNT\",\"BIT_GET\",\"BIT_LENGTH\",\"BROUND\",\"BTRIM\",\"CARDINALITY\",\"CBRT\",\"CEIL\",\"CEILING\",\"CHAR_LENGTH\",\"CHARACTER_LENGTH\",\"CHR\",\"CONCAT\",\"CONCAT_WS\",\"CONV\",\"COS\",\"COSH\",\"COT\",\"CRC32\",\"CURRENT_CATALOG\",\"CURRENT_DATABASE\",\"CURRENT_USER\",\"DEGREES\",\"ELT\",\"EXP\",\"EXPM1\",\"FACTORIAL\",\"FIND_IN_SET\",\"FLOOR\",\"FORALL\",\"FORMAT_NUMBER\",\"FORMAT_STRING\",\"FROM_CSV\",\"GETBIT\",\"HASH\",\"HEX\",\"HYPOT\",\"INITCAP\",\"INLINE\",\"INLINE_OUTER\",\"INPUT_FILE_BLOCK_LENGTH\",\"INPUT_FILE_BLOCK_START\",\"INPUT_FILE_NAME\",\"INSTR\",\"ISNAN\",\"ISNOTNULL\",\"ISNULL\",\"JAVA_METHOD\",\"LCASE\",\"LEFT\",\"LENGTH\",\"LEVENSHTEIN\",\"LN\",\"LOCATE\",\"LOG\",\"LOG10\",\"LOG1P\",\"LOG2\",\"LOWER\",\"LPAD\",\"LTRIM\",\"MAP_FILTER\",\"MAP_ZIP_WITH\",\"MD5\",\"MOD\",\"MONOTONICALLY_INCREASING_ID\",\"NAMED_STRUCT\",\"NANVL\",\"NEGATIVE\",\"NVL\",\"NVL2\",\"OCTET_LENGTH\",\"OVERLAY\",\"PARSE_URL\",\"PI\",\"PMOD\",\"POSEXPLODE\",\"POSEXPLODE_OUTER\",\"POSITION\",\"POSITIVE\",\"POW\",\"POWER\",\"PRINTF\",\"RADIANS\",\"RAISE_ERROR\",\"RAND\",\"RANDN\",\"RANDOM\",\"REFLECT\",\"REGEXP_EXTRACT\",\"REGEXP_EXTRACT_ALL\",\"REGEXP_LIKE\",\"REGEXP_REPLACE\",\"REPEAT\",\"REPLACE\",\"REVERSE\",\"RIGHT\",\"RINT\",\"ROUND\",\"RPAD\",\"RTRIM\",\"SCHEMA_OF_CSV\",\"SENTENCES\",\"SHA\",\"SHA1\",\"SHA2\",\"SHIFTLEFT\",\"SHIFTRIGHT\",\"SHIFTRIGHTUNSIGNED\",\"SIGN\",\"SIGNUM\",\"SIN\",\"SINH\",\"SOUNDEX\",\"SPACE\",\"SPARK_PARTITION_ID\",\"SPLIT\",\"SQRT\",\"STACK\",\"SUBSTR\",\"SUBSTRING\",\"SUBSTRING_INDEX\",\"TAN\",\"TANH\",\"TO_CSV\",\"TRANSFORM_KEYS\",\"TRANSFORM_VALUES\",\"TRANSLATE\",\"TRIM\",\"TRY_ADD\",\"TRY_DIVIDE\",\"TYPEOF\",\"UCASE\",\"UNBASE64\",\"UNHEX\",\"UPPER\",\"UUID\",\"VERSION\",\"WIDTH_BUCKET\",\"XPATH\",\"XPATH_BOOLEAN\",\"XPATH_DOUBLE\",\"XPATH_FLOAT\",\"XPATH_INT\",\"XPATH_LONG\",\"XPATH_NUMBER\",\"XPATH_SHORT\",\"XPATH_STRING\",\"XXHASH64\",\"ZIP_WITH\",\"CAST\",\"COALESCE\",\"NULLIF\"],io=v([\"SELECT [ALL | DISTINCT]\"]),ao=v([\"WITH\",\"FROM\",\"WHERE\",\"GROUP BY\",\"HAVING\",\"WINDOW\",\"PARTITION BY\",\"ORDER BY\",\"SORT BY\",\"CLUSTER BY\",\"DISTRIBUTE BY\",\"LIMIT\",\"INSERT [INTO | OVERWRITE] [TABLE]\",\"VALUES\",\"INSERT OVERWRITE [LOCAL] DIRECTORY\",\"LOAD DATA [LOCAL] INPATH\",\"[OVERWRITE] INTO TABLE\"]),Ir=v([\"CREATE [EXTERNAL] TABLE [IF NOT EXISTS]\"]),eT=v([\"CREATE [OR REPLACE] [GLOBAL TEMPORARY | TEMPORARY] VIEW [IF NOT EXISTS]\",\"DROP TABLE [IF EXISTS]\",\"ALTER TABLE\",\"ADD COLUMNS\",\"DROP {COLUMN | COLUMNS}\",\"RENAME TO\",\"RENAME COLUMN\",\"ALTER COLUMN\",\"TRUNCATE TABLE\",\"LATERAL VIEW\",\"ALTER DATABASE\",\"ALTER VIEW\",\"CREATE DATABASE\",\"CREATE FUNCTION\",\"DROP DATABASE\",\"DROP FUNCTION\",\"DROP VIEW\",\"REPAIR TABLE\",\"USE DATABASE\",\"TABLESAMPLE\",\"PIVOT\",\"TRANSFORM\",\"EXPLAIN\",\"ADD FILE\",\"ADD JAR\",\"ANALYZE TABLE\",\"CACHE TABLE\",\"CLEAR CACHE\",\"DESCRIBE DATABASE\",\"DESCRIBE FUNCTION\",\"DESCRIBE QUERY\",\"DESCRIBE TABLE\",\"LIST FILE\",\"LIST JAR\",\"REFRESH\",\"REFRESH TABLE\",\"REFRESH FUNCTION\",\"RESET\",\"SHOW COLUMNS\",\"SHOW CREATE TABLE\",\"SHOW DATABASES\",\"SHOW FUNCTIONS\",\"SHOW PARTITIONS\",\"SHOW TABLE EXTENDED\",\"SHOW TABLES\",\"SHOW TBLPROPERTIES\",\"SHOW VIEWS\",\"UNCACHE TABLE\"]),Io=v([\"UNION [ALL | DISTINCT]\",\"EXCEPT [ALL | DISTINCT]\",\"INTERSECT [ALL | DISTINCT]\"]),No=v([\"JOIN\",\"{LEFT | RIGHT | FULL} [OUTER] JOIN\",\"{INNER | CROSS} JOIN\",\"NATURAL [INNER] JOIN\",\"NATURAL {LEFT | RIGHT | FULL} [OUTER] JOIN\",\"[LEFT] {ANTI | SEMI} JOIN\",\"NATURAL [LEFT] {ANTI | SEMI} JOIN\"]),lo=v([\"ON DELETE\",\"ON UPDATE\",\"CURRENT ROW\",\"{ROWS | RANGE} BETWEEN\"]),_o={name:\"spark\",tokenizerOptions:{reservedSelect:io,reservedClauses:[...ao,...Ir,...eT],reservedSetOperations:Io,reservedJoins:No,reservedPhrases:lo,supportsXor:!0,reservedKeywords:So,reservedDataTypes:oo,reservedFunctionNames:Oo,extraParens:[\"[]\"],stringTypes:[\"''-bs\",'\"\"-bs',{quote:\"''-raw\",prefixes:[\"R\",\"X\"],requirePrefix:!0},{quote:'\"\"-raw',prefixes:[\"R\",\"X\"],requirePrefix:!0}],identTypes:[\"``\"],variableTypes:[{quote:\"{}\",prefixes:[\"$\"],requirePrefix:!0}],operators:[\"%\",\"~\",\"^\",\"|\",\"&\",\"<=>\",\"==\",\"!\",\"||\",\"->\"],postProcess:Lo},formatOptions:{onelineClauses:[...Ir,...eT],tabularOnelineClauses:eT}};function Lo(E){return E.map((e,T)=>{const t=E[T-1]||tt,r=E[T+1]||tt;return FE.WINDOW(e)&&r.type===\"OPEN_PAREN\"?AE(EE({},e),{type:\"RESERVED_FUNCTION_NAME\"}):e.text===\"ITEMS\"&&e.type===\"RESERVED_KEYWORD\"&&!(t.text===\"COLLECTION\"&&r.text===\"TERMINATED\")?AE(EE({},e),{type:\"IDENTIFIER\",text:e.raw}):e})}var Co=[\"ABS\",\"CHANGES\",\"CHAR\",\"COALESCE\",\"FORMAT\",\"GLOB\",\"HEX\",\"IFNULL\",\"IIF\",\"INSTR\",\"LAST_INSERT_ROWID\",\"LENGTH\",\"LIKE\",\"LIKELIHOOD\",\"LIKELY\",\"LOAD_EXTENSION\",\"LOWER\",\"LTRIM\",\"NULLIF\",\"PRINTF\",\"QUOTE\",\"RANDOM\",\"RANDOMBLOB\",\"REPLACE\",\"ROUND\",\"RTRIM\",\"SIGN\",\"SOUNDEX\",\"SQLITE_COMPILEOPTION_GET\",\"SQLITE_COMPILEOPTION_USED\",\"SQLITE_OFFSET\",\"SQLITE_SOURCE_ID\",\"SQLITE_VERSION\",\"SUBSTR\",\"SUBSTRING\",\"TOTAL_CHANGES\",\"TRIM\",\"TYPEOF\",\"UNICODE\",\"UNLIKELY\",\"UPPER\",\"ZEROBLOB\",\"AVG\",\"COUNT\",\"GROUP_CONCAT\",\"MAX\",\"MIN\",\"SUM\",\"TOTAL\",\"DATE\",\"TIME\",\"DATETIME\",\"JULIANDAY\",\"UNIXEPOCH\",\"STRFTIME\",\"row_number\",\"rank\",\"dense_rank\",\"percent_rank\",\"cume_dist\",\"ntile\",\"lag\",\"lead\",\"first_value\",\"last_value\",\"nth_value\",\"ACOS\",\"ACOSH\",\"ASIN\",\"ASINH\",\"ATAN\",\"ATAN2\",\"ATANH\",\"CEIL\",\"CEILING\",\"COS\",\"COSH\",\"DEGREES\",\"EXP\",\"FLOOR\",\"LN\",\"LOG\",\"LOG\",\"LOG10\",\"LOG2\",\"MOD\",\"PI\",\"POW\",\"POWER\",\"RADIANS\",\"SIN\",\"SINH\",\"SQRT\",\"TAN\",\"TANH\",\"TRUNC\",\"JSON\",\"JSON_ARRAY\",\"JSON_ARRAY_LENGTH\",\"JSON_ARRAY_LENGTH\",\"JSON_EXTRACT\",\"JSON_INSERT\",\"JSON_OBJECT\",\"JSON_PATCH\",\"JSON_REMOVE\",\"JSON_REPLACE\",\"JSON_SET\",\"JSON_TYPE\",\"JSON_TYPE\",\"JSON_VALID\",\"JSON_QUOTE\",\"JSON_GROUP_ARRAY\",\"JSON_GROUP_OBJECT\",\"JSON_EACH\",\"JSON_TREE\",\"CAST\"],uo=[\"ABORT\",\"ACTION\",\"ADD\",\"AFTER\",\"ALL\",\"ALTER\",\"AND\",\"ARE\",\"ALWAYS\",\"ANALYZE\",\"AS\",\"ASC\",\"ATTACH\",\"AUTOINCREMENT\",\"BEFORE\",\"BEGIN\",\"BETWEEN\",\"BY\",\"CASCADE\",\"CASE\",\"CAST\",\"CHECK\",\"COLLATE\",\"COLUMN\",\"COMMIT\",\"CONFLICT\",\"CONSTRAINT\",\"CREATE\",\"CROSS\",\"CURRENT\",\"CURRENT_DATE\",\"CURRENT_TIME\",\"CURRENT_TIMESTAMP\",\"DATABASE\",\"DEFAULT\",\"DEFERRABLE\",\"DEFERRED\",\"DELETE\",\"DESC\",\"DETACH\",\"DISTINCT\",\"DO\",\"DROP\",\"EACH\",\"ELSE\",\"END\",\"ESCAPE\",\"EXCEPT\",\"EXCLUDE\",\"EXCLUSIVE\",\"EXISTS\",\"EXPLAIN\",\"FAIL\",\"FILTER\",\"FIRST\",\"FOLLOWING\",\"FOR\",\"FOREIGN\",\"FROM\",\"FULL\",\"GENERATED\",\"GLOB\",\"GROUP\",\"GROUPS\",\"HAVING\",\"IF\",\"IGNORE\",\"IMMEDIATE\",\"IN\",\"INDEX\",\"INDEXED\",\"INITIALLY\",\"INNER\",\"INSERT\",\"INSTEAD\",\"INTERSECT\",\"INTO\",\"IS\",\"ISNULL\",\"JOIN\",\"KEY\",\"LAST\",\"LEFT\",\"LIKE\",\"LIMIT\",\"MATCH\",\"MATERIALIZED\",\"NATURAL\",\"NO\",\"NOT\",\"NOTHING\",\"NOTNULL\",\"NULL\",\"NULLS\",\"OF\",\"OFFSET\",\"ON\",\"ONLY\",\"OPEN\",\"OR\",\"ORDER\",\"OTHERS\",\"OUTER\",\"OVER\",\"PARTITION\",\"PLAN\",\"PRAGMA\",\"PRECEDING\",\"PRIMARY\",\"QUERY\",\"RAISE\",\"RANGE\",\"RECURSIVE\",\"REFERENCES\",\"REGEXP\",\"REINDEX\",\"RELEASE\",\"RENAME\",\"REPLACE\",\"RESTRICT\",\"RETURNING\",\"RIGHT\",\"ROLLBACK\",\"ROW\",\"ROWS\",\"SAVEPOINT\",\"SELECT\",\"SET\",\"TABLE\",\"TEMP\",\"TEMPORARY\",\"THEN\",\"TIES\",\"TO\",\"TRANSACTION\",\"TRIGGER\",\"UNBOUNDED\",\"UNION\",\"UNIQUE\",\"UPDATE\",\"USING\",\"VACUUM\",\"VALUES\",\"VIEW\",\"VIRTUAL\",\"WHEN\",\"WHERE\",\"WINDOW\",\"WITH\",\"WITHOUT\"],co=[\"ANY\",\"ARRAY\",\"BLOB\",\"CHARACTER\",\"DECIMAL\",\"INT\",\"INTEGER\",\"NATIVE CHARACTER\",\"NCHAR\",\"NUMERIC\",\"NVARCHAR\",\"REAL\",\"TEXT\",\"VARCHAR\",\"VARYING CHARACTER\"],fo=v([\"SELECT [ALL | DISTINCT]\"]),Po=v([\"WITH [RECURSIVE]\",\"FROM\",\"WHERE\",\"GROUP BY\",\"HAVING\",\"WINDOW\",\"PARTITION BY\",\"ORDER BY\",\"LIMIT\",\"OFFSET\",\"INSERT [OR ABORT | OR FAIL | OR IGNORE | OR REPLACE | OR ROLLBACK] INTO\",\"REPLACE INTO\",\"VALUES\",\"SET\"]),Nr=v([\"CREATE [TEMPORARY | TEMP] TABLE [IF NOT EXISTS]\"]),ET=v([\"CREATE [TEMPORARY | TEMP] VIEW [IF NOT EXISTS]\",\"UPDATE [OR ABORT | OR FAIL | OR IGNORE | OR REPLACE | OR ROLLBACK]\",\"ON CONFLICT\",\"DELETE FROM\",\"DROP TABLE [IF EXISTS]\",\"ALTER TABLE\",\"ADD [COLUMN]\",\"DROP [COLUMN]\",\"RENAME [COLUMN]\",\"RENAME TO\",\"SET SCHEMA\"]),Do=v([\"UNION [ALL]\",\"EXCEPT\",\"INTERSECT\"]),po=v([\"JOIN\",\"{LEFT | RIGHT | FULL} [OUTER] JOIN\",\"{INNER | CROSS} JOIN\",\"NATURAL [INNER] JOIN\",\"NATURAL {LEFT | RIGHT | FULL} [OUTER] JOIN\"]),Mo=v([\"ON {UPDATE | DELETE} [SET NULL | SET DEFAULT]\",\"{ROWS | RANGE | GROUPS} BETWEEN\"]),Uo={name:\"sqlite\",tokenizerOptions:{reservedSelect:fo,reservedClauses:[...Po,...Nr,...ET],reservedSetOperations:Do,reservedJoins:po,reservedPhrases:Mo,reservedKeywords:uo,reservedDataTypes:co,reservedFunctionNames:Co,stringTypes:[\"''-qq\",{quote:\"''-raw\",prefixes:[\"X\"],requirePrefix:!0}],identTypes:['\"\"-qq',\"``\",\"[]\"],paramTypes:{positional:!0,numbered:[\"?\"],named:[\":\",\"@\",\"$\"]},operators:[\"%\",\"~\",\"&\",\"|\",\"<<\",\">>\",\"==\",\"->\",\"->>\",\"||\"]},formatOptions:{onelineClauses:[...Nr,...ET],tabularOnelineClauses:ET}},mo=[\"GROUPING\",\"RANK\",\"DENSE_RANK\",\"PERCENT_RANK\",\"CUME_DIST\",\"ROW_NUMBER\",\"POSITION\",\"OCCURRENCES_REGEX\",\"POSITION_REGEX\",\"EXTRACT\",\"CHAR_LENGTH\",\"CHARACTER_LENGTH\",\"OCTET_LENGTH\",\"CARDINALITY\",\"ABS\",\"MOD\",\"LN\",\"EXP\",\"POWER\",\"SQRT\",\"FLOOR\",\"CEIL\",\"CEILING\",\"WIDTH_BUCKET\",\"SUBSTRING\",\"SUBSTRING_REGEX\",\"UPPER\",\"LOWER\",\"CONVERT\",\"TRANSLATE\",\"TRANSLATE_REGEX\",\"TRIM\",\"OVERLAY\",\"NORMALIZE\",\"SPECIFICTYPE\",\"CURRENT_DATE\",\"CURRENT_TIME\",\"LOCALTIME\",\"CURRENT_TIMESTAMP\",\"LOCALTIMESTAMP\",\"COUNT\",\"AVG\",\"MAX\",\"MIN\",\"SUM\",\"STDDEV_POP\",\"STDDEV_SAMP\",\"VAR_SAMP\",\"VAR_POP\",\"COLLECT\",\"FUSION\",\"INTERSECTION\",\"COVAR_POP\",\"COVAR_SAMP\",\"CORR\",\"REGR_SLOPE\",\"REGR_INTERCEPT\",\"REGR_COUNT\",\"REGR_R2\",\"REGR_AVGX\",\"REGR_AVGY\",\"REGR_SXX\",\"REGR_SYY\",\"REGR_SXY\",\"PERCENTILE_CONT\",\"PERCENTILE_DISC\",\"CAST\",\"COALESCE\",\"NULLIF\",\"ROUND\",\"SIN\",\"COS\",\"TAN\",\"ASIN\",\"ACOS\",\"ATAN\"],ho=[\"ALL\",\"ALLOCATE\",\"ALTER\",\"ANY\",\"ARE\",\"AS\",\"ASC\",\"ASENSITIVE\",\"ASYMMETRIC\",\"AT\",\"ATOMIC\",\"AUTHORIZATION\",\"BEGIN\",\"BETWEEN\",\"BOTH\",\"BY\",\"CALL\",\"CALLED\",\"CASCADED\",\"CAST\",\"CHECK\",\"CLOSE\",\"COALESCE\",\"COLLATE\",\"COLUMN\",\"COMMIT\",\"CONDITION\",\"CONNECT\",\"CONSTRAINT\",\"CORRESPONDING\",\"CREATE\",\"CROSS\",\"CUBE\",\"CURRENT\",\"CURRENT_CATALOG\",\"CURRENT_DEFAULT_TRANSFORM_GROUP\",\"CURRENT_PATH\",\"CURRENT_ROLE\",\"CURRENT_SCHEMA\",\"CURRENT_TRANSFORM_GROUP_FOR_TYPE\",\"CURRENT_USER\",\"CURSOR\",\"CYCLE\",\"DEALLOCATE\",\"DAY\",\"DECLARE\",\"DEFAULT\",\"DELETE\",\"DEREF\",\"DESC\",\"DESCRIBE\",\"DETERMINISTIC\",\"DISCONNECT\",\"DISTINCT\",\"DROP\",\"DYNAMIC\",\"EACH\",\"ELEMENT\",\"END-EXEC\",\"ESCAPE\",\"EVERY\",\"EXCEPT\",\"EXEC\",\"EXECUTE\",\"EXISTS\",\"EXTERNAL\",\"FALSE\",\"FETCH\",\"FILTER\",\"FOR\",\"FOREIGN\",\"FREE\",\"FROM\",\"FULL\",\"FUNCTION\",\"GET\",\"GLOBAL\",\"GRANT\",\"GROUP\",\"HAVING\",\"HOLD\",\"HOUR\",\"IDENTITY\",\"IN\",\"INDICATOR\",\"INNER\",\"INOUT\",\"INSENSITIVE\",\"INSERT\",\"INTERSECT\",\"INTO\",\"IS\",\"LANGUAGE\",\"LARGE\",\"LATERAL\",\"LEADING\",\"LEFT\",\"LIKE\",\"LIKE_REGEX\",\"LOCAL\",\"MATCH\",\"MEMBER\",\"MERGE\",\"METHOD\",\"MINUTE\",\"MODIFIES\",\"MODULE\",\"MONTH\",\"NATURAL\",\"NEW\",\"NO\",\"NONE\",\"NOT\",\"NULL\",\"NULLIF\",\"OF\",\"OLD\",\"ON\",\"ONLY\",\"OPEN\",\"ORDER\",\"OUT\",\"OUTER\",\"OVER\",\"OVERLAPS\",\"PARAMETER\",\"PARTITION\",\"PRECISION\",\"PREPARE\",\"PRIMARY\",\"PROCEDURE\",\"RANGE\",\"READS\",\"REAL\",\"RECURSIVE\",\"REF\",\"REFERENCES\",\"REFERENCING\",\"RELEASE\",\"RESULT\",\"RETURN\",\"RETURNS\",\"REVOKE\",\"RIGHT\",\"ROLLBACK\",\"ROLLUP\",\"ROW\",\"ROWS\",\"SAVEPOINT\",\"SCOPE\",\"SCROLL\",\"SEARCH\",\"SECOND\",\"SELECT\",\"SENSITIVE\",\"SESSION_USER\",\"SET\",\"SIMILAR\",\"SOME\",\"SPECIFIC\",\"SQL\",\"SQLEXCEPTION\",\"SQLSTATE\",\"SQLWARNING\",\"START\",\"STATIC\",\"SUBMULTISET\",\"SYMMETRIC\",\"SYSTEM\",\"SYSTEM_USER\",\"TABLE\",\"TABLESAMPLE\",\"THEN\",\"TIMEZONE_HOUR\",\"TIMEZONE_MINUTE\",\"TO\",\"TRAILING\",\"TRANSLATION\",\"TREAT\",\"TRIGGER\",\"TRUE\",\"UESCAPE\",\"UNION\",\"UNIQUE\",\"UNKNOWN\",\"UNNEST\",\"UPDATE\",\"USER\",\"USING\",\"VALUE\",\"VALUES\",\"WHENEVER\",\"WINDOW\",\"WITHIN\",\"WITHOUT\",\"YEAR\"],Go=[\"ARRAY\",\"BIGINT\",\"BINARY LARGE OBJECT\",\"BINARY VARYING\",\"BINARY\",\"BLOB\",\"BOOLEAN\",\"CHAR LARGE OBJECT\",\"CHAR VARYING\",\"CHAR\",\"CHARACTER LARGE OBJECT\",\"CHARACTER VARYING\",\"CHARACTER\",\"CLOB\",\"DATE\",\"DEC\",\"DECIMAL\",\"DOUBLE\",\"FLOAT\",\"INT\",\"INTEGER\",\"INTERVAL\",\"MULTISET\",\"NATIONAL CHAR VARYING\",\"NATIONAL CHAR\",\"NATIONAL CHARACTER LARGE OBJECT\",\"NATIONAL CHARACTER VARYING\",\"NATIONAL CHARACTER\",\"NCHAR LARGE OBJECT\",\"NCHAR VARYING\",\"NCHAR\",\"NCLOB\",\"NUMERIC\",\"SMALLINT\",\"TIME\",\"TIMESTAMP\",\"VARBINARY\",\"VARCHAR\"],go=v([\"SELECT [ALL | DISTINCT]\"]),Ho=v([\"WITH [RECURSIVE]\",\"FROM\",\"WHERE\",\"GROUP BY [ALL | DISTINCT]\",\"HAVING\",\"WINDOW\",\"PARTITION BY\",\"ORDER BY\",\"LIMIT\",\"OFFSET\",\"FETCH {FIRST | NEXT}\",\"INSERT INTO\",\"VALUES\",\"SET\"]),lr=v([\"CREATE [GLOBAL TEMPORARY | LOCAL TEMPORARY] TABLE\"]),tT=v([\"CREATE [RECURSIVE] VIEW\",\"UPDATE\",\"WHERE CURRENT OF\",\"DELETE FROM\",\"DROP TABLE\",\"ALTER TABLE\",\"ADD COLUMN\",\"DROP [COLUMN]\",\"RENAME COLUMN\",\"RENAME TO\",\"ALTER [COLUMN]\",\"{SET | DROP} DEFAULT\",\"ADD SCOPE\",\"DROP SCOPE {CASCADE | RESTRICT}\",\"RESTART WITH\",\"TRUNCATE TABLE\",\"SET SCHEMA\"]),bo=v([\"UNION [ALL | DISTINCT]\",\"EXCEPT [ALL | DISTINCT]\",\"INTERSECT [ALL | DISTINCT]\"]),yo=v([\"JOIN\",\"{LEFT | RIGHT | FULL} [OUTER] JOIN\",\"{INNER | CROSS} JOIN\",\"NATURAL [INNER] JOIN\",\"NATURAL {LEFT | RIGHT | FULL} [OUTER] JOIN\"]),Bo=v([\"ON {UPDATE | DELETE} [SET NULL | SET DEFAULT]\",\"{ROWS | RANGE} BETWEEN\"]),vo={name:\"sql\",tokenizerOptions:{reservedSelect:go,reservedClauses:[...Ho,...lr,...tT],reservedSetOperations:bo,reservedJoins:yo,reservedPhrases:Bo,reservedKeywords:ho,reservedDataTypes:Go,reservedFunctionNames:mo,stringTypes:[{quote:\"''-qq-bs\",prefixes:[\"N\",\"U&\"]},{quote:\"''-raw\",prefixes:[\"X\"],requirePrefix:!0}],identTypes:['\"\"-qq',\"``\"],paramTypes:{positional:!0},operators:[\"||\"]},formatOptions:{onelineClauses:[...lr,...tT],tabularOnelineClauses:tT}},Fo=[\"ABS\",\"ACOS\",\"ALL_MATCH\",\"ANY_MATCH\",\"APPROX_DISTINCT\",\"APPROX_MOST_FREQUENT\",\"APPROX_PERCENTILE\",\"APPROX_SET\",\"ARBITRARY\",\"ARRAYS_OVERLAP\",\"ARRAY_AGG\",\"ARRAY_DISTINCT\",\"ARRAY_EXCEPT\",\"ARRAY_INTERSECT\",\"ARRAY_JOIN\",\"ARRAY_MAX\",\"ARRAY_MIN\",\"ARRAY_POSITION\",\"ARRAY_REMOVE\",\"ARRAY_SORT\",\"ARRAY_UNION\",\"ASIN\",\"ATAN\",\"ATAN2\",\"AT_TIMEZONE\",\"AVG\",\"BAR\",\"BETA_CDF\",\"BING_TILE\",\"BING_TILES_AROUND\",\"BING_TILE_AT\",\"BING_TILE_COORDINATES\",\"BING_TILE_POLYGON\",\"BING_TILE_QUADKEY\",\"BING_TILE_ZOOM_LEVEL\",\"BITWISE_AND\",\"BITWISE_AND_AGG\",\"BITWISE_LEFT_SHIFT\",\"BITWISE_NOT\",\"BITWISE_OR\",\"BITWISE_OR_AGG\",\"BITWISE_RIGHT_SHIFT\",\"BITWISE_RIGHT_SHIFT_ARITHMETIC\",\"BITWISE_XOR\",\"BIT_COUNT\",\"BOOL_AND\",\"BOOL_OR\",\"CARDINALITY\",\"CAST\",\"CBRT\",\"CEIL\",\"CEILING\",\"CHAR2HEXINT\",\"CHECKSUM\",\"CHR\",\"CLASSIFY\",\"COALESCE\",\"CODEPOINT\",\"COLOR\",\"COMBINATIONS\",\"CONCAT\",\"CONCAT_WS\",\"CONTAINS\",\"CONTAINS_SEQUENCE\",\"CONVEX_HULL_AGG\",\"CORR\",\"COS\",\"COSH\",\"COSINE_SIMILARITY\",\"COUNT\",\"COUNT_IF\",\"COVAR_POP\",\"COVAR_SAMP\",\"CRC32\",\"CUME_DIST\",\"CURRENT_CATALOG\",\"CURRENT_DATE\",\"CURRENT_GROUPS\",\"CURRENT_SCHEMA\",\"CURRENT_TIME\",\"CURRENT_TIMESTAMP\",\"CURRENT_TIMEZONE\",\"CURRENT_USER\",\"DATE\",\"DATE_ADD\",\"DATE_DIFF\",\"DATE_FORMAT\",\"DATE_PARSE\",\"DATE_TRUNC\",\"DAY\",\"DAY_OF_MONTH\",\"DAY_OF_WEEK\",\"DAY_OF_YEAR\",\"DEGREES\",\"DENSE_RANK\",\"DOW\",\"DOY\",\"E\",\"ELEMENT_AT\",\"EMPTY_APPROX_SET\",\"EVALUATE_CLASSIFIER_PREDICTIONS\",\"EVERY\",\"EXP\",\"EXTRACT\",\"FEATURES\",\"FILTER\",\"FIRST_VALUE\",\"FLATTEN\",\"FLOOR\",\"FORMAT\",\"FORMAT_DATETIME\",\"FORMAT_NUMBER\",\"FROM_BASE\",\"FROM_BASE32\",\"FROM_BASE64\",\"FROM_BASE64URL\",\"FROM_BIG_ENDIAN_32\",\"FROM_BIG_ENDIAN_64\",\"FROM_ENCODED_POLYLINE\",\"FROM_GEOJSON_GEOMETRY\",\"FROM_HEX\",\"FROM_IEEE754_32\",\"FROM_IEEE754_64\",\"FROM_ISO8601_DATE\",\"FROM_ISO8601_TIMESTAMP\",\"FROM_ISO8601_TIMESTAMP_NANOS\",\"FROM_UNIXTIME\",\"FROM_UNIXTIME_NANOS\",\"FROM_UTF8\",\"GEOMETRIC_MEAN\",\"GEOMETRY_FROM_HADOOP_SHAPE\",\"GEOMETRY_INVALID_REASON\",\"GEOMETRY_NEAREST_POINTS\",\"GEOMETRY_TO_BING_TILES\",\"GEOMETRY_UNION\",\"GEOMETRY_UNION_AGG\",\"GREATEST\",\"GREAT_CIRCLE_DISTANCE\",\"HAMMING_DISTANCE\",\"HASH_COUNTS\",\"HISTOGRAM\",\"HMAC_MD5\",\"HMAC_SHA1\",\"HMAC_SHA256\",\"HMAC_SHA512\",\"HOUR\",\"HUMAN_READABLE_SECONDS\",\"IF\",\"INDEX\",\"INFINITY\",\"INTERSECTION_CARDINALITY\",\"INVERSE_BETA_CDF\",\"INVERSE_NORMAL_CDF\",\"IS_FINITE\",\"IS_INFINITE\",\"IS_JSON_SCALAR\",\"IS_NAN\",\"JACCARD_INDEX\",\"JSON_ARRAY_CONTAINS\",\"JSON_ARRAY_GET\",\"JSON_ARRAY_LENGTH\",\"JSON_EXISTS\",\"JSON_EXTRACT\",\"JSON_EXTRACT_SCALAR\",\"JSON_FORMAT\",\"JSON_PARSE\",\"JSON_QUERY\",\"JSON_SIZE\",\"JSON_VALUE\",\"KURTOSIS\",\"LAG\",\"LAST_DAY_OF_MONTH\",\"LAST_VALUE\",\"LEAD\",\"LEARN_CLASSIFIER\",\"LEARN_LIBSVM_CLASSIFIER\",\"LEARN_LIBSVM_REGRESSOR\",\"LEARN_REGRESSOR\",\"LEAST\",\"LENGTH\",\"LEVENSHTEIN_DISTANCE\",\"LINE_INTERPOLATE_POINT\",\"LINE_INTERPOLATE_POINTS\",\"LINE_LOCATE_POINT\",\"LISTAGG\",\"LN\",\"LOCALTIME\",\"LOCALTIMESTAMP\",\"LOG\",\"LOG10\",\"LOG2\",\"LOWER\",\"LPAD\",\"LTRIM\",\"LUHN_CHECK\",\"MAKE_SET_DIGEST\",\"MAP\",\"MAP_AGG\",\"MAP_CONCAT\",\"MAP_ENTRIES\",\"MAP_FILTER\",\"MAP_FROM_ENTRIES\",\"MAP_KEYS\",\"MAP_UNION\",\"MAP_VALUES\",\"MAP_ZIP_WITH\",\"MAX\",\"MAX_BY\",\"MD5\",\"MERGE\",\"MERGE_SET_DIGEST\",\"MILLISECOND\",\"MIN\",\"MINUTE\",\"MIN_BY\",\"MOD\",\"MONTH\",\"MULTIMAP_AGG\",\"MULTIMAP_FROM_ENTRIES\",\"MURMUR3\",\"NAN\",\"NGRAMS\",\"NONE_MATCH\",\"NORMALIZE\",\"NORMAL_CDF\",\"NOW\",\"NTH_VALUE\",\"NTILE\",\"NULLIF\",\"NUMERIC_HISTOGRAM\",\"OBJECTID\",\"OBJECTID_TIMESTAMP\",\"PARSE_DATA_SIZE\",\"PARSE_DATETIME\",\"PARSE_DURATION\",\"PERCENT_RANK\",\"PI\",\"POSITION\",\"POW\",\"POWER\",\"QDIGEST_AGG\",\"QUARTER\",\"RADIANS\",\"RAND\",\"RANDOM\",\"RANK\",\"REDUCE\",\"REDUCE_AGG\",\"REGEXP_COUNT\",\"REGEXP_EXTRACT\",\"REGEXP_EXTRACT_ALL\",\"REGEXP_LIKE\",\"REGEXP_POSITION\",\"REGEXP_REPLACE\",\"REGEXP_SPLIT\",\"REGRESS\",\"REGR_INTERCEPT\",\"REGR_SLOPE\",\"RENDER\",\"REPEAT\",\"REPLACE\",\"REVERSE\",\"RGB\",\"ROUND\",\"ROW_NUMBER\",\"RPAD\",\"RTRIM\",\"SECOND\",\"SEQUENCE\",\"SHA1\",\"SHA256\",\"SHA512\",\"SHUFFLE\",\"SIGN\",\"SIMPLIFY_GEOMETRY\",\"SIN\",\"SKEWNESS\",\"SLICE\",\"SOUNDEX\",\"SPATIAL_PARTITIONING\",\"SPATIAL_PARTITIONS\",\"SPLIT\",\"SPLIT_PART\",\"SPLIT_TO_MAP\",\"SPLIT_TO_MULTIMAP\",\"SPOOKY_HASH_V2_32\",\"SPOOKY_HASH_V2_64\",\"SQRT\",\"STARTS_WITH\",\"STDDEV\",\"STDDEV_POP\",\"STDDEV_SAMP\",\"STRPOS\",\"ST_AREA\",\"ST_ASBINARY\",\"ST_ASTEXT\",\"ST_BOUNDARY\",\"ST_BUFFER\",\"ST_CENTROID\",\"ST_CONTAINS\",\"ST_CONVEXHULL\",\"ST_COORDDIM\",\"ST_CROSSES\",\"ST_DIFFERENCE\",\"ST_DIMENSION\",\"ST_DISJOINT\",\"ST_DISTANCE\",\"ST_ENDPOINT\",\"ST_ENVELOPE\",\"ST_ENVELOPEASPTS\",\"ST_EQUALS\",\"ST_EXTERIORRING\",\"ST_GEOMETRIES\",\"ST_GEOMETRYFROMTEXT\",\"ST_GEOMETRYN\",\"ST_GEOMETRYTYPE\",\"ST_GEOMFROMBINARY\",\"ST_INTERIORRINGN\",\"ST_INTERIORRINGS\",\"ST_INTERSECTION\",\"ST_INTERSECTS\",\"ST_ISCLOSED\",\"ST_ISEMPTY\",\"ST_ISRING\",\"ST_ISSIMPLE\",\"ST_ISVALID\",\"ST_LENGTH\",\"ST_LINEFROMTEXT\",\"ST_LINESTRING\",\"ST_MULTIPOINT\",\"ST_NUMGEOMETRIES\",\"ST_NUMINTERIORRING\",\"ST_NUMPOINTS\",\"ST_OVERLAPS\",\"ST_POINT\",\"ST_POINTN\",\"ST_POINTS\",\"ST_POLYGON\",\"ST_RELATE\",\"ST_STARTPOINT\",\"ST_SYMDIFFERENCE\",\"ST_TOUCHES\",\"ST_UNION\",\"ST_WITHIN\",\"ST_X\",\"ST_XMAX\",\"ST_XMIN\",\"ST_Y\",\"ST_YMAX\",\"ST_YMIN\",\"SUBSTR\",\"SUBSTRING\",\"SUM\",\"TAN\",\"TANH\",\"TDIGEST_AGG\",\"TIMESTAMP_OBJECTID\",\"TIMEZONE_HOUR\",\"TIMEZONE_MINUTE\",\"TO_BASE\",\"TO_BASE32\",\"TO_BASE64\",\"TO_BASE64URL\",\"TO_BIG_ENDIAN_32\",\"TO_BIG_ENDIAN_64\",\"TO_CHAR\",\"TO_DATE\",\"TO_ENCODED_POLYLINE\",\"TO_GEOJSON_GEOMETRY\",\"TO_GEOMETRY\",\"TO_HEX\",\"TO_IEEE754_32\",\"TO_IEEE754_64\",\"TO_ISO8601\",\"TO_MILLISECONDS\",\"TO_SPHERICAL_GEOGRAPHY\",\"TO_TIMESTAMP\",\"TO_UNIXTIME\",\"TO_UTF8\",\"TRANSFORM\",\"TRANSFORM_KEYS\",\"TRANSFORM_VALUES\",\"TRANSLATE\",\"TRIM\",\"TRIM_ARRAY\",\"TRUNCATE\",\"TRY\",\"TRY_CAST\",\"TYPEOF\",\"UPPER\",\"URL_DECODE\",\"URL_ENCODE\",\"URL_EXTRACT_FRAGMENT\",\"URL_EXTRACT_HOST\",\"URL_EXTRACT_PARAMETER\",\"URL_EXTRACT_PATH\",\"URL_EXTRACT_PORT\",\"URL_EXTRACT_PROTOCOL\",\"URL_EXTRACT_QUERY\",\"UUID\",\"VALUES_AT_QUANTILES\",\"VALUE_AT_QUANTILE\",\"VARIANCE\",\"VAR_POP\",\"VAR_SAMP\",\"VERSION\",\"WEEK\",\"WEEK_OF_YEAR\",\"WIDTH_BUCKET\",\"WILSON_INTERVAL_LOWER\",\"WILSON_INTERVAL_UPPER\",\"WITH_TIMEZONE\",\"WORD_STEM\",\"XXHASH64\",\"YEAR\",\"YEAR_OF_WEEK\",\"YOW\",\"ZIP\",\"ZIP_WITH\",\"CLASSIFIER\",\"FIRST\",\"LAST\",\"MATCH_NUMBER\",\"NEXT\",\"PERMUTE\",\"PREV\"],Yo=[\"ABSENT\",\"ADD\",\"ADMIN\",\"AFTER\",\"ALL\",\"ALTER\",\"ANALYZE\",\"AND\",\"ANY\",\"AS\",\"ASC\",\"AT\",\"AUTHORIZATION\",\"BERNOULLI\",\"BETWEEN\",\"BOTH\",\"BY\",\"CALL\",\"CASCADE\",\"CASE\",\"CATALOGS\",\"COLUMN\",\"COLUMNS\",\"COMMENT\",\"COMMIT\",\"COMMITTED\",\"CONDITIONAL\",\"CONSTRAINT\",\"COPARTITION\",\"CREATE\",\"CROSS\",\"CUBE\",\"CURRENT\",\"CURRENT_PATH\",\"CURRENT_ROLE\",\"DATA\",\"DEALLOCATE\",\"DEFAULT\",\"DEFINE\",\"DEFINER\",\"DELETE\",\"DENY\",\"DESC\",\"DESCRIBE\",\"DESCRIPTOR\",\"DISTINCT\",\"DISTRIBUTED\",\"DOUBLE\",\"DROP\",\"ELSE\",\"EMPTY\",\"ENCODING\",\"END\",\"ERROR\",\"ESCAPE\",\"EXCEPT\",\"EXCLUDING\",\"EXECUTE\",\"EXISTS\",\"EXPLAIN\",\"FALSE\",\"FETCH\",\"FINAL\",\"FIRST\",\"FOLLOWING\",\"FOR\",\"FROM\",\"FULL\",\"FUNCTIONS\",\"GRANT\",\"GRANTED\",\"GRANTS\",\"GRAPHVIZ\",\"GROUP\",\"GROUPING\",\"GROUPS\",\"HAVING\",\"IGNORE\",\"IN\",\"INCLUDING\",\"INITIAL\",\"INNER\",\"INPUT\",\"INSERT\",\"INTERSECT\",\"INTERVAL\",\"INTO\",\"INVOKER\",\"IO\",\"IS\",\"ISOLATION\",\"JOIN\",\"JSON\",\"JSON_ARRAY\",\"JSON_OBJECT\",\"KEEP\",\"KEY\",\"KEYS\",\"LAST\",\"LATERAL\",\"LEADING\",\"LEFT\",\"LEVEL\",\"LIKE\",\"LIMIT\",\"LOCAL\",\"LOGICAL\",\"MATCH\",\"MATCHED\",\"MATCHES\",\"MATCH_RECOGNIZE\",\"MATERIALIZED\",\"MEASURES\",\"NATURAL\",\"NEXT\",\"NFC\",\"NFD\",\"NFKC\",\"NFKD\",\"NO\",\"NONE\",\"NOT\",\"NULL\",\"NULLS\",\"OBJECT\",\"OF\",\"OFFSET\",\"OMIT\",\"ON\",\"ONE\",\"ONLY\",\"OPTION\",\"OR\",\"ORDER\",\"ORDINALITY\",\"OUTER\",\"OUTPUT\",\"OVER\",\"OVERFLOW\",\"PARTITION\",\"PARTITIONS\",\"PASSING\",\"PAST\",\"PATH\",\"PATTERN\",\"PER\",\"PERMUTE\",\"PRECEDING\",\"PRECISION\",\"PREPARE\",\"PRIVILEGES\",\"PROPERTIES\",\"PRUNE\",\"QUOTES\",\"RANGE\",\"READ\",\"RECURSIVE\",\"REFRESH\",\"RENAME\",\"REPEATABLE\",\"RESET\",\"RESPECT\",\"RESTRICT\",\"RETURNING\",\"REVOKE\",\"RIGHT\",\"ROLE\",\"ROLES\",\"ROLLBACK\",\"ROLLUP\",\"ROW\",\"ROWS\",\"RUNNING\",\"SCALAR\",\"SCHEMA\",\"SCHEMAS\",\"SECURITY\",\"SEEK\",\"SELECT\",\"SERIALIZABLE\",\"SESSION\",\"SET\",\"SETS\",\"SHOW\",\"SKIP\",\"SOME\",\"START\",\"STATS\",\"STRING\",\"SUBSET\",\"SYSTEM\",\"TABLE\",\"TABLES\",\"TABLESAMPLE\",\"TEXT\",\"THEN\",\"TIES\",\"TIME\",\"TIMESTAMP\",\"TO\",\"TRAILING\",\"TRANSACTION\",\"TRUE\",\"TYPE\",\"UESCAPE\",\"UNBOUNDED\",\"UNCOMMITTED\",\"UNCONDITIONAL\",\"UNION\",\"UNIQUE\",\"UNKNOWN\",\"UNMATCHED\",\"UNNEST\",\"UPDATE\",\"USE\",\"USER\",\"USING\",\"UTF16\",\"UTF32\",\"UTF8\",\"VALIDATE\",\"VALUE\",\"VALUES\",\"VERBOSE\",\"VIEW\",\"WHEN\",\"WHERE\",\"WINDOW\",\"WITH\",\"WITHIN\",\"WITHOUT\",\"WORK\",\"WRAPPER\",\"WRITE\",\"ZONE\"],Vo=[\"BIGINT\",\"INT\",\"INTEGER\",\"SMALLINT\",\"TINYINT\",\"BOOLEAN\",\"DATE\",\"DECIMAL\",\"REAL\",\"DOUBLE\",\"HYPERLOGLOG\",\"QDIGEST\",\"TDIGEST\",\"P4HYPERLOGLOG\",\"INTERVAL\",\"TIMESTAMP\",\"TIME\",\"VARBINARY\",\"VARCHAR\",\"CHAR\",\"ROW\",\"ARRAY\",\"MAP\",\"JSON\",\"JSON2016\",\"IPADDRESS\",\"GEOMETRY\",\"UUID\",\"SETDIGEST\",\"JONIREGEXP\",\"RE2JREGEXP\",\"LIKEPATTERN\",\"COLOR\",\"CODEPOINTS\",\"FUNCTION\",\"JSONPATH\"],Wo=v([\"SELECT [ALL | DISTINCT]\"]),wo=v([\"WITH [RECURSIVE]\",\"FROM\",\"WHERE\",\"GROUP BY [ALL | DISTINCT]\",\"HAVING\",\"WINDOW\",\"PARTITION BY\",\"ORDER BY\",\"LIMIT\",\"OFFSET\",\"FETCH {FIRST | NEXT}\",\"INSERT INTO\",\"VALUES\",\"SET\",\"MATCH_RECOGNIZE\",\"MEASURES\",\"ONE ROW PER MATCH\",\"ALL ROWS PER MATCH\",\"AFTER MATCH\",\"PATTERN\",\"SUBSET\",\"DEFINE\"]),_r=v([\"CREATE TABLE [IF NOT EXISTS]\"]),TT=v([\"CREATE [OR REPLACE] [MATERIALIZED] VIEW\",\"UPDATE\",\"DELETE FROM\",\"DROP TABLE [IF EXISTS]\",\"ALTER TABLE [IF EXISTS]\",\"ADD COLUMN [IF NOT EXISTS]\",\"DROP COLUMN [IF EXISTS]\",\"RENAME COLUMN [IF EXISTS]\",\"RENAME TO\",\"SET AUTHORIZATION [USER | ROLE]\",\"SET PROPERTIES\",\"EXECUTE\",\"TRUNCATE TABLE\",\"ALTER SCHEMA\",\"ALTER MATERIALIZED VIEW\",\"ALTER VIEW\",\"CREATE SCHEMA\",\"CREATE ROLE\",\"DROP SCHEMA\",\"DROP MATERIALIZED VIEW\",\"DROP VIEW\",\"DROP ROLE\",\"EXPLAIN\",\"ANALYZE\",\"EXPLAIN ANALYZE\",\"EXPLAIN ANALYZE VERBOSE\",\"USE\",\"DESCRIBE INPUT\",\"DESCRIBE OUTPUT\",\"REFRESH MATERIALIZED VIEW\",\"RESET SESSION\",\"SET SESSION\",\"SET PATH\",\"SET TIME ZONE\",\"SHOW GRANTS\",\"SHOW CREATE TABLE\",\"SHOW CREATE SCHEMA\",\"SHOW CREATE VIEW\",\"SHOW CREATE MATERIALIZED VIEW\",\"SHOW TABLES\",\"SHOW SCHEMAS\",\"SHOW CATALOGS\",\"SHOW COLUMNS\",\"SHOW STATS FOR\",\"SHOW ROLES\",\"SHOW CURRENT ROLES\",\"SHOW ROLE GRANTS\",\"SHOW FUNCTIONS\",\"SHOW SESSION\"]),$o=v([\"UNION [ALL | DISTINCT]\",\"EXCEPT [ALL | DISTINCT]\",\"INTERSECT [ALL | DISTINCT]\"]),xo=v([\"JOIN\",\"{LEFT | RIGHT | FULL} [OUTER] JOIN\",\"{INNER | CROSS} JOIN\",\"NATURAL [INNER] JOIN\",\"NATURAL {LEFT | RIGHT | FULL} [OUTER] JOIN\"]),Xo=v([\"{ROWS | RANGE | GROUPS} BETWEEN\",\"IS [NOT] DISTINCT FROM\"]),ko={name:\"trino\",tokenizerOptions:{reservedSelect:Wo,reservedClauses:[...wo,..._r,...TT],reservedSetOperations:$o,reservedJoins:xo,reservedPhrases:Xo,reservedKeywords:Yo,reservedDataTypes:Vo,reservedFunctionNames:Fo,extraParens:[\"[]\",\"{}\"],stringTypes:[{quote:\"''-qq\",prefixes:[\"U&\"]},{quote:\"''-raw\",prefixes:[\"X\"],requirePrefix:!0}],identTypes:['\"\"-qq'],paramTypes:{positional:!0},operators:[\"%\",\"->\",\"=>\",\":\",\"||\",\"|\",\"^\",\"$\"]},formatOptions:{onelineClauses:[..._r,...TT],tabularOnelineClauses:TT}},Ko=[\"APPROX_COUNT_DISTINCT\",\"AVG\",\"CHECKSUM_AGG\",\"COUNT\",\"COUNT_BIG\",\"GROUPING\",\"GROUPING_ID\",\"MAX\",\"MIN\",\"STDEV\",\"STDEVP\",\"SUM\",\"VAR\",\"VARP\",\"CUME_DIST\",\"FIRST_VALUE\",\"LAG\",\"LAST_VALUE\",\"LEAD\",\"PERCENTILE_CONT\",\"PERCENTILE_DISC\",\"PERCENT_RANK\",\"Collation - COLLATIONPROPERTY\",\"Collation - TERTIARY_WEIGHTS\",\"@@DBTS\",\"@@LANGID\",\"@@LANGUAGE\",\"@@LOCK_TIMEOUT\",\"@@MAX_CONNECTIONS\",\"@@MAX_PRECISION\",\"@@NESTLEVEL\",\"@@OPTIONS\",\"@@REMSERVER\",\"@@SERVERNAME\",\"@@SERVICENAME\",\"@@SPID\",\"@@TEXTSIZE\",\"@@VERSION\",\"CAST\",\"CONVERT\",\"PARSE\",\"TRY_CAST\",\"TRY_CONVERT\",\"TRY_PARSE\",\"ASYMKEY_ID\",\"ASYMKEYPROPERTY\",\"CERTPROPERTY\",\"CERT_ID\",\"CRYPT_GEN_RANDOM\",\"DECRYPTBYASYMKEY\",\"DECRYPTBYCERT\",\"DECRYPTBYKEY\",\"DECRYPTBYKEYAUTOASYMKEY\",\"DECRYPTBYKEYAUTOCERT\",\"DECRYPTBYPASSPHRASE\",\"ENCRYPTBYASYMKEY\",\"ENCRYPTBYCERT\",\"ENCRYPTBYKEY\",\"ENCRYPTBYPASSPHRASE\",\"HASHBYTES\",\"IS_OBJECTSIGNED\",\"KEY_GUID\",\"KEY_ID\",\"KEY_NAME\",\"SIGNBYASYMKEY\",\"SIGNBYCERT\",\"SYMKEYPROPERTY\",\"VERIFYSIGNEDBYCERT\",\"VERIFYSIGNEDBYASYMKEY\",\"@@CURSOR_ROWS\",\"@@FETCH_STATUS\",\"CURSOR_STATUS\",\"DATALENGTH\",\"IDENT_CURRENT\",\"IDENT_INCR\",\"IDENT_SEED\",\"IDENTITY\",\"SQL_VARIANT_PROPERTY\",\"@@DATEFIRST\",\"CURRENT_TIMESTAMP\",\"CURRENT_TIMEZONE\",\"CURRENT_TIMEZONE_ID\",\"DATEADD\",\"DATEDIFF\",\"DATEDIFF_BIG\",\"DATEFROMPARTS\",\"DATENAME\",\"DATEPART\",\"DATETIME2FROMPARTS\",\"DATETIMEFROMPARTS\",\"DATETIMEOFFSETFROMPARTS\",\"DAY\",\"EOMONTH\",\"GETDATE\",\"GETUTCDATE\",\"ISDATE\",\"MONTH\",\"SMALLDATETIMEFROMPARTS\",\"SWITCHOFFSET\",\"SYSDATETIME\",\"SYSDATETIMEOFFSET\",\"SYSUTCDATETIME\",\"TIMEFROMPARTS\",\"TODATETIMEOFFSET\",\"YEAR\",\"JSON\",\"ISJSON\",\"JSON_VALUE\",\"JSON_QUERY\",\"JSON_MODIFY\",\"ABS\",\"ACOS\",\"ASIN\",\"ATAN\",\"ATN2\",\"CEILING\",\"COS\",\"COT\",\"DEGREES\",\"EXP\",\"FLOOR\",\"LOG\",\"LOG10\",\"PI\",\"POWER\",\"RADIANS\",\"RAND\",\"ROUND\",\"SIGN\",\"SIN\",\"SQRT\",\"SQUARE\",\"TAN\",\"CHOOSE\",\"GREATEST\",\"IIF\",\"LEAST\",\"@@PROCID\",\"APP_NAME\",\"APPLOCK_MODE\",\"APPLOCK_TEST\",\"ASSEMBLYPROPERTY\",\"COL_LENGTH\",\"COL_NAME\",\"COLUMNPROPERTY\",\"DATABASEPROPERTYEX\",\"DB_ID\",\"DB_NAME\",\"FILE_ID\",\"FILE_IDEX\",\"FILE_NAME\",\"FILEGROUP_ID\",\"FILEGROUP_NAME\",\"FILEGROUPPROPERTY\",\"FILEPROPERTY\",\"FILEPROPERTYEX\",\"FULLTEXTCATALOGPROPERTY\",\"FULLTEXTSERVICEPROPERTY\",\"INDEX_COL\",\"INDEXKEY_PROPERTY\",\"INDEXPROPERTY\",\"NEXT VALUE FOR\",\"OBJECT_DEFINITION\",\"OBJECT_ID\",\"OBJECT_NAME\",\"OBJECT_SCHEMA_NAME\",\"OBJECTPROPERTY\",\"OBJECTPROPERTYEX\",\"ORIGINAL_DB_NAME\",\"PARSENAME\",\"SCHEMA_ID\",\"SCHEMA_NAME\",\"SCOPE_IDENTITY\",\"SERVERPROPERTY\",\"STATS_DATE\",\"TYPE_ID\",\"TYPE_NAME\",\"TYPEPROPERTY\",\"DENSE_RANK\",\"NTILE\",\"RANK\",\"ROW_NUMBER\",\"PUBLISHINGSERVERNAME\",\"CERTENCODED\",\"CERTPRIVATEKEY\",\"CURRENT_USER\",\"DATABASE_PRINCIPAL_ID\",\"HAS_DBACCESS\",\"HAS_PERMS_BY_NAME\",\"IS_MEMBER\",\"IS_ROLEMEMBER\",\"IS_SRVROLEMEMBER\",\"LOGINPROPERTY\",\"ORIGINAL_LOGIN\",\"PERMISSIONS\",\"PWDENCRYPT\",\"PWDCOMPARE\",\"SESSION_USER\",\"SESSIONPROPERTY\",\"SUSER_ID\",\"SUSER_NAME\",\"SUSER_SID\",\"SUSER_SNAME\",\"SYSTEM_USER\",\"USER\",\"USER_ID\",\"USER_NAME\",\"ASCII\",\"CHAR\",\"CHARINDEX\",\"CONCAT\",\"CONCAT_WS\",\"DIFFERENCE\",\"FORMAT\",\"LEFT\",\"LEN\",\"LOWER\",\"LTRIM\",\"NCHAR\",\"PATINDEX\",\"QUOTENAME\",\"REPLACE\",\"REPLICATE\",\"REVERSE\",\"RIGHT\",\"RTRIM\",\"SOUNDEX\",\"SPACE\",\"STR\",\"STRING_AGG\",\"STRING_ESCAPE\",\"STUFF\",\"SUBSTRING\",\"TRANSLATE\",\"TRIM\",\"UNICODE\",\"UPPER\",\"$PARTITION\",\"@@ERROR\",\"@@IDENTITY\",\"@@PACK_RECEIVED\",\"@@ROWCOUNT\",\"@@TRANCOUNT\",\"BINARY_CHECKSUM\",\"CHECKSUM\",\"COMPRESS\",\"CONNECTIONPROPERTY\",\"CONTEXT_INFO\",\"CURRENT_REQUEST_ID\",\"CURRENT_TRANSACTION_ID\",\"DECOMPRESS\",\"ERROR_LINE\",\"ERROR_MESSAGE\",\"ERROR_NUMBER\",\"ERROR_PROCEDURE\",\"ERROR_SEVERITY\",\"ERROR_STATE\",\"FORMATMESSAGE\",\"GET_FILESTREAM_TRANSACTION_CONTEXT\",\"GETANSINULL\",\"HOST_ID\",\"HOST_NAME\",\"ISNULL\",\"ISNUMERIC\",\"MIN_ACTIVE_ROWVERSION\",\"NEWID\",\"NEWSEQUENTIALID\",\"ROWCOUNT_BIG\",\"SESSION_CONTEXT\",\"XACT_STATE\",\"@@CONNECTIONS\",\"@@CPU_BUSY\",\"@@IDLE\",\"@@IO_BUSY\",\"@@PACK_SENT\",\"@@PACKET_ERRORS\",\"@@TIMETICKS\",\"@@TOTAL_ERRORS\",\"@@TOTAL_READ\",\"@@TOTAL_WRITE\",\"TEXTPTR\",\"TEXTVALID\",\"COLUMNS_UPDATED\",\"EVENTDATA\",\"TRIGGER_NESTLEVEL\",\"UPDATE\",\"COALESCE\",\"NULLIF\"],Jo=[\"ADD\",\"ALL\",\"ALTER\",\"AND\",\"ANY\",\"AS\",\"ASC\",\"AUTHORIZATION\",\"BACKUP\",\"BEGIN\",\"BETWEEN\",\"BREAK\",\"BROWSE\",\"BULK\",\"BY\",\"CASCADE\",\"CHECK\",\"CHECKPOINT\",\"CLOSE\",\"CLUSTERED\",\"COALESCE\",\"COLLATE\",\"COLUMN\",\"COMMIT\",\"COMPUTE\",\"CONSTRAINT\",\"CONTAINS\",\"CONTAINSTABLE\",\"CONTINUE\",\"CONVERT\",\"CREATE\",\"CROSS\",\"CURRENT\",\"CURRENT_DATE\",\"CURRENT_TIME\",\"CURRENT_TIMESTAMP\",\"CURRENT_USER\",\"CURSOR\",\"DATABASE\",\"DBCC\",\"DEALLOCATE\",\"DECLARE\",\"DEFAULT\",\"DELETE\",\"DENY\",\"DESC\",\"DISK\",\"DISTINCT\",\"DISTRIBUTED\",\"DROP\",\"DUMP\",\"ERRLVL\",\"ESCAPE\",\"EXEC\",\"EXECUTE\",\"EXISTS\",\"EXIT\",\"EXTERNAL\",\"FETCH\",\"FILE\",\"FILLFACTOR\",\"FOR\",\"FOREIGN\",\"FREETEXT\",\"FREETEXTTABLE\",\"FROM\",\"FULL\",\"FUNCTION\",\"GOTO\",\"GRANT\",\"GROUP\",\"HAVING\",\"HOLDLOCK\",\"IDENTITY\",\"IDENTITYCOL\",\"IDENTITY_INSERT\",\"IF\",\"IN\",\"INDEX\",\"INNER\",\"INSERT\",\"INTERSECT\",\"INTO\",\"IS\",\"JOIN\",\"KEY\",\"KILL\",\"LEFT\",\"LIKE\",\"LINENO\",\"LOAD\",\"MERGE\",\"NOCHECK\",\"NONCLUSTERED\",\"NOT\",\"NULL\",\"NULLIF\",\"OF\",\"OFF\",\"OFFSETS\",\"ON\",\"OPEN\",\"OPENDATASOURCE\",\"OPENQUERY\",\"OPENROWSET\",\"OPENXML\",\"OPTION\",\"OR\",\"ORDER\",\"OUTER\",\"OVER\",\"PERCENT\",\"PIVOT\",\"PLAN\",\"PRIMARY\",\"PRINT\",\"PROC\",\"PROCEDURE\",\"PUBLIC\",\"RAISERROR\",\"READ\",\"READTEXT\",\"RECONFIGURE\",\"REFERENCES\",\"REPLICATION\",\"RESTORE\",\"RESTRICT\",\"RETURN\",\"REVERT\",\"REVOKE\",\"RIGHT\",\"ROLLBACK\",\"ROWCOUNT\",\"ROWGUIDCOL\",\"RULE\",\"SAVE\",\"SCHEMA\",\"SECURITYAUDIT\",\"SELECT\",\"SEMANTICKEYPHRASETABLE\",\"SEMANTICSIMILARITYDETAILSTABLE\",\"SEMANTICSIMILARITYTABLE\",\"SESSION_USER\",\"SET\",\"SETUSER\",\"SHUTDOWN\",\"SOME\",\"STATISTICS\",\"SYSTEM_USER\",\"TABLE\",\"TABLESAMPLE\",\"TEXTSIZE\",\"THEN\",\"TO\",\"TOP\",\"TRAN\",\"TRANSACTION\",\"TRIGGER\",\"TRUNCATE\",\"TRY_CONVERT\",\"TSEQUAL\",\"UNION\",\"UNIQUE\",\"UNPIVOT\",\"UPDATE\",\"UPDATETEXT\",\"USE\",\"USER\",\"VALUES\",\"VIEW\",\"WAITFOR\",\"WHERE\",\"WHILE\",\"WITH\",\"WITHIN GROUP\",\"WRITETEXT\",\"ABSOLUTE\",\"ACTION\",\"ADA\",\"ALLOCATE\",\"ARE\",\"ASSERTION\",\"AT\",\"AVG\",\"BIT_LENGTH\",\"BOTH\",\"CASCADED\",\"CAST\",\"CATALOG\",\"CHARACTER_LENGTH\",\"CHAR_LENGTH\",\"COLLATION\",\"CONNECT\",\"CONNECTION\",\"CONSTRAINTS\",\"CORRESPONDING\",\"COUNT\",\"DAY\",\"DEFERRABLE\",\"DEFERRED\",\"DESCRIBE\",\"DESCRIPTOR\",\"DIAGNOSTICS\",\"DISCONNECT\",\"DOMAIN\",\"END-EXEC\",\"EXCEPTION\",\"EXTRACT\",\"FALSE\",\"FIRST\",\"FORTRAN\",\"FOUND\",\"GET\",\"GLOBAL\",\"GO\",\"HOUR\",\"IMMEDIATE\",\"INCLUDE\",\"INDICATOR\",\"INITIALLY\",\"INPUT\",\"INSENSITIVE\",\"INTERVAL\",\"ISOLATION\",\"LANGUAGE\",\"LAST\",\"LEADING\",\"LEVEL\",\"LOCAL\",\"LOWER\",\"MATCH\",\"MAX\",\"MIN\",\"MINUTE\",\"MODULE\",\"MONTH\",\"NAMES\",\"NATURAL\",\"NEXT\",\"NO\",\"NONE\",\"OCTET_LENGTH\",\"ONLY\",\"OUTPUT\",\"OVERLAPS\",\"PAD\",\"PARTIAL\",\"PASCAL\",\"POSITION\",\"PREPARE\",\"PRESERVE\",\"PRIOR\",\"PRIVILEGES\",\"RELATIVE\",\"ROWS\",\"SCROLL\",\"SECOND\",\"SECTION\",\"SESSION\",\"SIZE\",\"SPACE\",\"SQL\",\"SQLCA\",\"SQLCODE\",\"SQLERROR\",\"SQLSTATE\",\"SQLWARNING\",\"SUBSTRING\",\"SUM\",\"TEMPORARY\",\"TIMEZONE_HOUR\",\"TIMEZONE_MINUTE\",\"TRAILING\",\"TRANSLATE\",\"TRANSLATION\",\"TRIM\",\"TRUE\",\"UNKNOWN\",\"UPPER\",\"USAGE\",\"VALUE\",\"WHENEVER\",\"WORK\",\"WRITE\",\"YEAR\",\"ZONE\"],qo=[\"BINARY\",\"BIT\",\"CHAR\",\"CHAR\",\"CHARACTER\",\"DATE\",\"DATETIME2\",\"DATETIMEOFFSET\",\"DEC\",\"DECIMAL\",\"DOUBLE\",\"FLOAT\",\"INT\",\"INTEGER\",\"NATIONAL\",\"NCHAR\",\"NUMERIC\",\"NVARCHAR\",\"PRECISION\",\"REAL\",\"SMALLINT\",\"TIME\",\"TIMESTAMP\",\"VARBINARY\",\"VARCHAR\"],Qo=v([\"SELECT [ALL | DISTINCT]\"]),Zo=v([\"WITH\",\"INTO\",\"FROM\",\"WHERE\",\"GROUP BY\",\"HAVING\",\"WINDOW\",\"PARTITION BY\",\"ORDER BY\",\"OFFSET\",\"FETCH {FIRST | NEXT}\",\"FOR {BROWSE | XML | JSON}\",\"OPTION\",\"INSERT [INTO]\",\"VALUES\",\"SET\",\"MERGE [INTO]\",\"WHEN [NOT] MATCHED [BY TARGET | BY SOURCE] [THEN]\",\"UPDATE SET\",\"CREATE [OR ALTER] {PROC | PROCEDURE}\"]),Lr=v([\"CREATE TABLE\"]),rT=v([\"CREATE [OR ALTER] [MATERIALIZED] VIEW\",\"UPDATE\",\"WHERE CURRENT OF\",\"DELETE [FROM]\",\"DROP TABLE [IF EXISTS]\",\"ALTER TABLE\",\"ADD\",\"DROP COLUMN [IF EXISTS]\",\"ALTER COLUMN\",\"TRUNCATE TABLE\",\"ADD SENSITIVITY CLASSIFICATION\",\"ADD SIGNATURE\",\"AGGREGATE\",\"ANSI_DEFAULTS\",\"ANSI_NULLS\",\"ANSI_NULL_DFLT_OFF\",\"ANSI_NULL_DFLT_ON\",\"ANSI_PADDING\",\"ANSI_WARNINGS\",\"APPLICATION ROLE\",\"ARITHABORT\",\"ARITHIGNORE\",\"ASSEMBLY\",\"ASYMMETRIC KEY\",\"AUTHORIZATION\",\"AVAILABILITY GROUP\",\"BACKUP\",\"BACKUP CERTIFICATE\",\"BACKUP MASTER KEY\",\"BACKUP SERVICE MASTER KEY\",\"BEGIN CONVERSATION TIMER\",\"BEGIN DIALOG CONVERSATION\",\"BROKER PRIORITY\",\"BULK INSERT\",\"CERTIFICATE\",\"CLOSE MASTER KEY\",\"CLOSE SYMMETRIC KEY\",\"COLLATE\",\"COLUMN ENCRYPTION KEY\",\"COLUMN MASTER KEY\",\"COLUMNSTORE INDEX\",\"CONCAT_NULL_YIELDS_NULL\",\"CONTEXT_INFO\",\"CONTRACT\",\"CREDENTIAL\",\"CRYPTOGRAPHIC PROVIDER\",\"CURSOR_CLOSE_ON_COMMIT\",\"DATABASE\",\"DATABASE AUDIT SPECIFICATION\",\"DATABASE ENCRYPTION KEY\",\"DATABASE HADR\",\"DATABASE SCOPED CONFIGURATION\",\"DATABASE SCOPED CREDENTIAL\",\"DATABASE SET\",\"DATEFIRST\",\"DATEFORMAT\",\"DEADLOCK_PRIORITY\",\"DENY\",\"DENY XML\",\"DISABLE TRIGGER\",\"ENABLE TRIGGER\",\"END CONVERSATION\",\"ENDPOINT\",\"EVENT NOTIFICATION\",\"EVENT SESSION\",\"EXECUTE AS\",\"EXTERNAL DATA SOURCE\",\"EXTERNAL FILE FORMAT\",\"EXTERNAL LANGUAGE\",\"EXTERNAL LIBRARY\",\"EXTERNAL RESOURCE POOL\",\"EXTERNAL TABLE\",\"FIPS_FLAGGER\",\"FMTONLY\",\"FORCEPLAN\",\"FULLTEXT CATALOG\",\"FULLTEXT INDEX\",\"FULLTEXT STOPLIST\",\"FUNCTION\",\"GET CONVERSATION GROUP\",\"GET_TRANSMISSION_STATUS\",\"GRANT\",\"GRANT XML\",\"IDENTITY_INSERT\",\"IMPLICIT_TRANSACTIONS\",\"INDEX\",\"LANGUAGE\",\"LOCK_TIMEOUT\",\"LOGIN\",\"MASTER KEY\",\"MESSAGE TYPE\",\"MOVE CONVERSATION\",\"NOCOUNT\",\"NOEXEC\",\"NUMERIC_ROUNDABORT\",\"OFFSETS\",\"OPEN MASTER KEY\",\"OPEN SYMMETRIC KEY\",\"PARSEONLY\",\"PARTITION FUNCTION\",\"PARTITION SCHEME\",\"PROCEDURE\",\"QUERY_GOVERNOR_COST_LIMIT\",\"QUEUE\",\"QUOTED_IDENTIFIER\",\"RECEIVE\",\"REMOTE SERVICE BINDING\",\"REMOTE_PROC_TRANSACTIONS\",\"RESOURCE GOVERNOR\",\"RESOURCE POOL\",\"RESTORE\",\"RESTORE FILELISTONLY\",\"RESTORE HEADERONLY\",\"RESTORE LABELONLY\",\"RESTORE MASTER KEY\",\"RESTORE REWINDONLY\",\"RESTORE SERVICE MASTER KEY\",\"RESTORE VERIFYONLY\",\"REVERT\",\"REVOKE\",\"REVOKE XML\",\"ROLE\",\"ROUTE\",\"ROWCOUNT\",\"RULE\",\"SCHEMA\",\"SEARCH PROPERTY LIST\",\"SECURITY POLICY\",\"SELECTIVE XML INDEX\",\"SEND\",\"SENSITIVITY CLASSIFICATION\",\"SEQUENCE\",\"SERVER AUDIT\",\"SERVER AUDIT SPECIFICATION\",\"SERVER CONFIGURATION\",\"SERVER ROLE\",\"SERVICE\",\"SERVICE MASTER KEY\",\"SETUSER\",\"SHOWPLAN_ALL\",\"SHOWPLAN_TEXT\",\"SHOWPLAN_XML\",\"SIGNATURE\",\"SPATIAL INDEX\",\"STATISTICS\",\"STATISTICS IO\",\"STATISTICS PROFILE\",\"STATISTICS TIME\",\"STATISTICS XML\",\"SYMMETRIC KEY\",\"SYNONYM\",\"TABLE\",\"TABLE IDENTITY\",\"TEXTSIZE\",\"TRANSACTION ISOLATION LEVEL\",\"TRIGGER\",\"TYPE\",\"UPDATE STATISTICS\",\"USER\",\"WORKLOAD GROUP\",\"XACT_ABORT\",\"XML INDEX\",\"XML SCHEMA COLLECTION\"]),jo=v([\"UNION [ALL]\",\"EXCEPT\",\"INTERSECT\"]),zo=v([\"JOIN\",\"{LEFT | RIGHT | FULL} [OUTER] JOIN\",\"{INNER | CROSS} JOIN\",\"{CROSS | OUTER} APPLY\"]),eO=v([\"ON {UPDATE | DELETE} [SET NULL | SET DEFAULT]\",\"{ROWS | RANGE} BETWEEN\"]),EO={name:\"transactsql\",tokenizerOptions:{reservedSelect:Qo,reservedClauses:[...Zo,...Lr,...rT],reservedSetOperations:jo,reservedJoins:zo,reservedPhrases:eO,reservedKeywords:Jo,reservedDataTypes:qo,reservedFunctionNames:Ko,nestedBlockComments:!0,stringTypes:[{quote:\"''-qq\",prefixes:[\"N\"]}],identTypes:['\"\"-qq',\"[]\"],identChars:{first:\"#@\",rest:\"#@$\"},paramTypes:{named:[\"@\"],quoted:[\"@\"]},operators:[\"%\",\"&\",\"|\",\"^\",\"~\",\"!<\",\"!>\",\"+=\",\"-=\",\"*=\",\"/=\",\"%=\",\"|=\",\"&=\",\"^=\",\"::\",\":\"],propertyAccessOperators:[\"..\"]},formatOptions:{alwaysDenseOperators:[\"::\"],onelineClauses:[...Lr,...rT],tabularOnelineClauses:rT}},tO=[\"ADD\",\"ALL\",\"ALTER\",\"ANALYZE\",\"AND\",\"AS\",\"ASC\",\"ASENSITIVE\",\"BEFORE\",\"BETWEEN\",\"_BINARY\",\"BOTH\",\"BY\",\"CALL\",\"CASCADE\",\"CASE\",\"CHANGE\",\"CHECK\",\"COLLATE\",\"COLUMN\",\"CONDITION\",\"CONSTRAINT\",\"CONTINUE\",\"CONVERT\",\"CREATE\",\"CROSS\",\"CURRENT_DATE\",\"CURRENT_TIME\",\"CURRENT_TIMESTAMP\",\"CURRENT_USER\",\"CURSOR\",\"DATABASE\",\"DATABASES\",\"DAY_HOUR\",\"DAY_MICROSECOND\",\"DAY_MINUTE\",\"DAY_SECOND\",\"DECLARE\",\"DEFAULT\",\"DELAYED\",\"DELETE\",\"DESC\",\"DESCRIBE\",\"DETERMINISTIC\",\"DISTINCT\",\"DISTINCTROW\",\"DIV\",\"DROP\",\"DUAL\",\"EACH\",\"ELSE\",\"ELSEIF\",\"ENCLOSED\",\"ESCAPED\",\"EXCEPT\",\"EXISTS\",\"EXIT\",\"EXPLAIN\",\"EXTRA_JOIN\",\"FALSE\",\"FETCH\",\"FOR\",\"FORCE\",\"FORCE_COMPILED_MODE\",\"FORCE_INTERPRETER_MODE\",\"FOREIGN\",\"FROM\",\"FULL\",\"FULLTEXT\",\"GRANT\",\"GROUP\",\"HAVING\",\"HEARTBEAT_NO_LOGGING\",\"HIGH_PRIORITY\",\"HOUR_MICROSECOND\",\"HOUR_MINUTE\",\"HOUR_SECOND\",\"IF\",\"IGNORE\",\"IN\",\"INDEX\",\"INFILE\",\"INNER\",\"INOUT\",\"INSENSITIVE\",\"INSERT\",\"IN\",\"_INTERNAL_DYNAMIC_TYPECAST\",\"INTERSECT\",\"INTERVAL\",\"INTO\",\"ITERATE\",\"JOIN\",\"KEY\",\"KEYS\",\"KILL\",\"LEADING\",\"LEAVE\",\"LEFT\",\"LIKE\",\"LIMIT\",\"LINES\",\"LOAD\",\"LOCALTIME\",\"LOCALTIMESTAMP\",\"LOCK\",\"LOOP\",\"LOW_PRIORITY\",\"MATCH\",\"MAXVALUE\",\"MINUS\",\"MINUTE_MICROSECOND\",\"MINUTE_SECOND\",\"MOD\",\"MODIFIES\",\"NATURAL\",\"NO_QUERY_REWRITE\",\"NOT\",\"NO_WRITE_TO_BINLOG\",\"NO_QUERY_REWRITE\",\"NULL\",\"ON\",\"OPTIMIZE\",\"OPTION\",\"OPTIONALLY\",\"OR\",\"ORDER\",\"OUT\",\"OUTER\",\"OUTFILE\",\"OVER\",\"PRIMARY\",\"PROCEDURE\",\"PURGE\",\"RANGE\",\"READ\",\"READS\",\"REFERENCES\",\"REGEXP\",\"RELEASE\",\"RENAME\",\"REPEAT\",\"REPLACE\",\"REQUIRE\",\"RESTRICT\",\"RETURN\",\"REVOKE\",\"RIGHT\",\"RIGHT_ANTI_JOIN\",\"RIGHT_SEMI_JOIN\",\"RIGHT_STRAIGHT_JOIN\",\"RLIKE\",\"SCHEMA\",\"SCHEMAS\",\"SECOND_MICROSECOND\",\"SELECT\",\"SEMI_JOIN\",\"SENSITIVE\",\"SEPARATOR\",\"SET\",\"SHOW\",\"SIGNAL\",\"SPATIAL\",\"SPECIFIC\",\"SQL\",\"SQL_BIG_RESULT\",\"SQL_BUFFER_RESULT\",\"SQL_CACHE\",\"SQL_CALC_FOUND_ROWS\",\"SQLEXCEPTION\",\"SQL_NO_CACHE\",\"SQL_NO_LOGGING\",\"SQL_SMALL_RESULT\",\"SQLSTATE\",\"SQLWARNING\",\"STRAIGHT_JOIN\",\"TABLE\",\"TERMINATED\",\"THEN\",\"TO\",\"TRAILING\",\"TRIGGER\",\"TRUE\",\"UNBOUNDED\",\"UNDO\",\"UNION\",\"UNIQUE\",\"UNLOCK\",\"UPDATE\",\"USAGE\",\"USE\",\"USING\",\"UTC_DATE\",\"UTC_TIME\",\"UTC_TIMESTAMP\",\"_UTF8\",\"VALUES\",\"WHEN\",\"WHERE\",\"WHILE\",\"WINDOW\",\"WITH\",\"WITHIN\",\"WRITE\",\"XOR\",\"YEAR_MONTH\",\"ZEROFILL\"],TO=[\"BIGINT\",\"BINARY\",\"BIT\",\"BLOB\",\"CHAR\",\"CHARACTER\",\"DATETIME\",\"DEC\",\"DECIMAL\",\"DOUBLE PRECISION\",\"DOUBLE\",\"ENUM\",\"FIXED\",\"FLOAT\",\"FLOAT4\",\"FLOAT8\",\"INT\",\"INT1\",\"INT2\",\"INT3\",\"INT4\",\"INT8\",\"INTEGER\",\"LONG\",\"LONGBLOB\",\"LONGTEXT\",\"MEDIUMBLOB\",\"MEDIUMINT\",\"MEDIUMTEXT\",\"MIDDLEINT\",\"NATIONAL CHAR\",\"NATIONAL VARCHAR\",\"NUMERIC\",\"PRECISION\",\"REAL\",\"SMALLINT\",\"TEXT\",\"TIME\",\"TIMESTAMP\",\"TINYBLOB\",\"TINYINT\",\"TINYTEXT\",\"UNSIGNED\",\"VARBINARY\",\"VARCHAR\",\"VARCHARACTER\",\"YEAR\"],rO=[\"ABS\",\"ACOS\",\"ADDDATE\",\"ADDTIME\",\"AES_DECRYPT\",\"AES_ENCRYPT\",\"ANY_VALUE\",\"APPROX_COUNT_DISTINCT\",\"APPROX_COUNT_DISTINCT_ACCUMULATE\",\"APPROX_COUNT_DISTINCT_COMBINE\",\"APPROX_COUNT_DISTINCT_ESTIMATE\",\"APPROX_GEOGRAPHY_INTERSECTS\",\"APPROX_PERCENTILE\",\"ASCII\",\"ASIN\",\"ATAN\",\"ATAN2\",\"AVG\",\"BIN\",\"BINARY\",\"BIT_AND\",\"BIT_COUNT\",\"BIT_OR\",\"BIT_XOR\",\"CAST\",\"CEIL\",\"CEILING\",\"CHAR\",\"CHARACTER_LENGTH\",\"CHAR_LENGTH\",\"CHARSET\",\"COALESCE\",\"COERCIBILITY\",\"COLLATION\",\"COLLECT\",\"CONCAT\",\"CONCAT_WS\",\"CONNECTION_ID\",\"CONV\",\"CONVERT\",\"CONVERT_TZ\",\"COS\",\"COT\",\"COUNT\",\"CUME_DIST\",\"CURDATE\",\"CURRENT_DATE\",\"CURRENT_ROLE\",\"CURRENT_TIME\",\"CURRENT_TIMESTAMP\",\"CURRENT_USER\",\"CURTIME\",\"DATABASE\",\"DATE\",\"DATE_ADD\",\"DATEDIFF\",\"DATE_FORMAT\",\"DATE_SUB\",\"DATE_TRUNC\",\"DAY\",\"DAYNAME\",\"DAYOFMONTH\",\"DAYOFWEEK\",\"DAYOFYEAR\",\"DECODE\",\"DEFAULT\",\"DEGREES\",\"DENSE_RANK\",\"DIV\",\"DOT_PRODUCT\",\"ELT\",\"EUCLIDEAN_DISTANCE\",\"EXP\",\"EXTRACT\",\"FIELD\",\"FIRST\",\"FIRST_VALUE\",\"FLOOR\",\"FORMAT\",\"FOUND_ROWS\",\"FROM_BASE64\",\"FROM_DAYS\",\"FROM_UNIXTIME\",\"GEOGRAPHY_AREA\",\"GEOGRAPHY_CONTAINS\",\"GEOGRAPHY_DISTANCE\",\"GEOGRAPHY_INTERSECTS\",\"GEOGRAPHY_LATITUDE\",\"GEOGRAPHY_LENGTH\",\"GEOGRAPHY_LONGITUDE\",\"GEOGRAPHY_POINT\",\"GEOGRAPHY_WITHIN_DISTANCE\",\"GEOMETRY_AREA\",\"GEOMETRY_CONTAINS\",\"GEOMETRY_DISTANCE\",\"GEOMETRY_FILTER\",\"GEOMETRY_INTERSECTS\",\"GEOMETRY_LENGTH\",\"GEOMETRY_POINT\",\"GEOMETRY_WITHIN_DISTANCE\",\"GEOMETRY_X\",\"GEOMETRY_Y\",\"GREATEST\",\"GROUPING\",\"GROUP_CONCAT\",\"HEX\",\"HIGHLIGHT\",\"HOUR\",\"ICU_VERSION\",\"IF\",\"IFNULL\",\"INET_ATON\",\"INET_NTOA\",\"INET6_ATON\",\"INET6_NTOA\",\"INITCAP\",\"INSERT\",\"INSTR\",\"INTERVAL\",\"IS\",\"IS NULL\",\"JSON_AGG\",\"JSON_ARRAY_CONTAINS_DOUBLE\",\"JSON_ARRAY_CONTAINS_JSON\",\"JSON_ARRAY_CONTAINS_STRING\",\"JSON_ARRAY_PUSH_DOUBLE\",\"JSON_ARRAY_PUSH_JSON\",\"JSON_ARRAY_PUSH_STRING\",\"JSON_DELETE_KEY\",\"JSON_EXTRACT_DOUBLE\",\"JSON_EXTRACT_JSON\",\"JSON_EXTRACT_STRING\",\"JSON_EXTRACT_BIGINT\",\"JSON_GET_TYPE\",\"JSON_LENGTH\",\"JSON_SET_DOUBLE\",\"JSON_SET_JSON\",\"JSON_SET_STRING\",\"JSON_SPLICE_DOUBLE\",\"JSON_SPLICE_JSON\",\"JSON_SPLICE_STRING\",\"LAG\",\"LAST_DAY\",\"LAST_VALUE\",\"LCASE\",\"LEAD\",\"LEAST\",\"LEFT\",\"LENGTH\",\"LIKE\",\"LN\",\"LOCALTIME\",\"LOCALTIMESTAMP\",\"LOCATE\",\"LOG\",\"LOG10\",\"LOG2\",\"LPAD\",\"LTRIM\",\"MATCH\",\"MAX\",\"MD5\",\"MEDIAN\",\"MICROSECOND\",\"MIN\",\"MINUTE\",\"MOD\",\"MONTH\",\"MONTHNAME\",\"MONTHS_BETWEEN\",\"NOT\",\"NOW\",\"NTH_VALUE\",\"NTILE\",\"NULLIF\",\"OCTET_LENGTH\",\"PERCENT_RANK\",\"PERCENTILE_CONT\",\"PERCENTILE_DISC\",\"PI\",\"PIVOT\",\"POSITION\",\"POW\",\"POWER\",\"QUARTER\",\"QUOTE\",\"RADIANS\",\"RAND\",\"RANK\",\"REGEXP\",\"REPEAT\",\"REPLACE\",\"REVERSE\",\"RIGHT\",\"RLIKE\",\"ROUND\",\"ROW_COUNT\",\"ROW_NUMBER\",\"RPAD\",\"RTRIM\",\"SCALAR\",\"SCHEMA\",\"SEC_TO_TIME\",\"SHA1\",\"SHA2\",\"SIGMOID\",\"SIGN\",\"SIN\",\"SLEEP\",\"SPLIT\",\"SOUNDEX\",\"SOUNDS LIKE\",\"SOURCE_POS_WAIT\",\"SPACE\",\"SQRT\",\"STDDEV\",\"STDDEV_POP\",\"STDDEV_SAMP\",\"STR_TO_DATE\",\"SUBDATE\",\"SUBSTR\",\"SUBSTRING\",\"SUBSTRING_INDEX\",\"SUM\",\"SYS_GUID\",\"TAN\",\"TIME\",\"TIMEDIFF\",\"TIME_BUCKET\",\"TIME_FORMAT\",\"TIMESTAMP\",\"TIMESTAMPADD\",\"TIMESTAMPDIFF\",\"TIME_TO_SEC\",\"TO_BASE64\",\"TO_CHAR\",\"TO_DAYS\",\"TO_JSON\",\"TO_NUMBER\",\"TO_SECONDS\",\"TO_TIMESTAMP\",\"TRIM\",\"TRUNC\",\"TRUNCATE\",\"UCASE\",\"UNHEX\",\"UNIX_TIMESTAMP\",\"UPDATEXML\",\"UPPER\",\"UTC_DATE\",\"UTC_TIME\",\"UTC_TIMESTAMP\",\"UUID\",\"VALUES\",\"VARIANCE\",\"VAR_POP\",\"VAR_SAMP\",\"VECTOR_SUB\",\"VERSION\",\"WEEK\",\"WEEKDAY\",\"WEEKOFYEAR\",\"YEAR\"],RO=v([\"SELECT [ALL | DISTINCT | DISTINCTROW]\"]),nO=v([\"WITH\",\"FROM\",\"WHERE\",\"GROUP BY\",\"HAVING\",\"PARTITION BY\",\"ORDER BY\",\"LIMIT\",\"OFFSET\",\"INSERT [IGNORE] [INTO]\",\"VALUES\",\"REPLACE [INTO]\",\"ON DUPLICATE KEY UPDATE\",\"SET\",\"CREATE [OR REPLACE] [TEMPORARY] PROCEDURE [IF NOT EXISTS]\",\"CREATE [OR REPLACE] [EXTERNAL] FUNCTION\"]),Cr=v([\"CREATE [ROWSTORE] [REFERENCE | TEMPORARY | GLOBAL TEMPORARY] TABLE [IF NOT EXISTS]\"]),RT=v([\"CREATE VIEW\",\"UPDATE\",\"DELETE [FROM]\",\"DROP [TEMPORARY] TABLE [IF EXISTS]\",\"ALTER [ONLINE] TABLE\",\"ADD [COLUMN]\",\"ADD [UNIQUE] {INDEX | KEY}\",\"DROP [COLUMN]\",\"MODIFY [COLUMN]\",\"CHANGE\",\"RENAME [TO | AS]\",\"TRUNCATE [TABLE]\",\"ADD AGGREGATOR\",\"ADD LEAF\",\"AGGREGATOR SET AS MASTER\",\"ALTER DATABASE\",\"ALTER PIPELINE\",\"ALTER RESOURCE POOL\",\"ALTER USER\",\"ALTER VIEW\",\"ANALYZE TABLE\",\"ATTACH DATABASE\",\"ATTACH LEAF\",\"ATTACH LEAF ALL\",\"BACKUP DATABASE\",\"BINLOG\",\"BOOTSTRAP AGGREGATOR\",\"CACHE INDEX\",\"CALL\",\"CHANGE\",\"CHANGE MASTER TO\",\"CHANGE REPLICATION FILTER\",\"CHANGE REPLICATION SOURCE TO\",\"CHECK BLOB CHECKSUM\",\"CHECK TABLE\",\"CHECKSUM TABLE\",\"CLEAR ORPHAN DATABASES\",\"CLONE\",\"COMMIT\",\"CREATE DATABASE\",\"CREATE GROUP\",\"CREATE INDEX\",\"CREATE LINK\",\"CREATE MILESTONE\",\"CREATE PIPELINE\",\"CREATE RESOURCE POOL\",\"CREATE ROLE\",\"CREATE USER\",\"DEALLOCATE PREPARE\",\"DESCRIBE\",\"DETACH DATABASE\",\"DETACH PIPELINE\",\"DROP DATABASE\",\"DROP FUNCTION\",\"DROP INDEX\",\"DROP LINK\",\"DROP PIPELINE\",\"DROP PROCEDURE\",\"DROP RESOURCE POOL\",\"DROP ROLE\",\"DROP USER\",\"DROP VIEW\",\"EXECUTE\",\"EXPLAIN\",\"FLUSH\",\"FORCE\",\"GRANT\",\"HANDLER\",\"HELP\",\"KILL CONNECTION\",\"KILLALL QUERIES\",\"LOAD DATA\",\"LOAD INDEX INTO CACHE\",\"LOAD XML\",\"LOCK INSTANCE FOR BACKUP\",\"LOCK TABLES\",\"MASTER_POS_WAIT\",\"OPTIMIZE TABLE\",\"PREPARE\",\"PURGE BINARY LOGS\",\"REBALANCE PARTITIONS\",\"RELEASE SAVEPOINT\",\"REMOVE AGGREGATOR\",\"REMOVE LEAF\",\"REPAIR TABLE\",\"REPLACE\",\"REPLICATE DATABASE\",\"RESET\",\"RESET MASTER\",\"RESET PERSIST\",\"RESET REPLICA\",\"RESET SLAVE\",\"RESTART\",\"RESTORE DATABASE\",\"RESTORE REDUNDANCY\",\"REVOKE\",\"ROLLBACK\",\"ROLLBACK TO SAVEPOINT\",\"SAVEPOINT\",\"SET CHARACTER SET\",\"SET DEFAULT ROLE\",\"SET NAMES\",\"SET PASSWORD\",\"SET RESOURCE GROUP\",\"SET ROLE\",\"SET TRANSACTION\",\"SHOW\",\"SHOW CHARACTER SET\",\"SHOW COLLATION\",\"SHOW COLUMNS\",\"SHOW CREATE DATABASE\",\"SHOW CREATE FUNCTION\",\"SHOW CREATE PIPELINE\",\"SHOW CREATE PROCEDURE\",\"SHOW CREATE TABLE\",\"SHOW CREATE USER\",\"SHOW CREATE VIEW\",\"SHOW DATABASES\",\"SHOW ENGINE\",\"SHOW ENGINES\",\"SHOW ERRORS\",\"SHOW FUNCTION CODE\",\"SHOW FUNCTION STATUS\",\"SHOW GRANTS\",\"SHOW INDEX\",\"SHOW MASTER STATUS\",\"SHOW OPEN TABLES\",\"SHOW PLUGINS\",\"SHOW PRIVILEGES\",\"SHOW PROCEDURE CODE\",\"SHOW PROCEDURE STATUS\",\"SHOW PROCESSLIST\",\"SHOW PROFILE\",\"SHOW PROFILES\",\"SHOW RELAYLOG EVENTS\",\"SHOW REPLICA STATUS\",\"SHOW REPLICAS\",\"SHOW SLAVE\",\"SHOW SLAVE HOSTS\",\"SHOW STATUS\",\"SHOW TABLE STATUS\",\"SHOW TABLES\",\"SHOW VARIABLES\",\"SHOW WARNINGS\",\"SHUTDOWN\",\"SNAPSHOT DATABASE\",\"SOURCE_POS_WAIT\",\"START GROUP_REPLICATION\",\"START PIPELINE\",\"START REPLICA\",\"START SLAVE\",\"START TRANSACTION\",\"STOP GROUP_REPLICATION\",\"STOP PIPELINE\",\"STOP REPLICA\",\"STOP REPLICATING\",\"STOP SLAVE\",\"TEST PIPELINE\",\"UNLOCK INSTANCE\",\"UNLOCK TABLES\",\"USE\",\"XA\",\"ITERATE\",\"LEAVE\",\"LOOP\",\"REPEAT\",\"RETURN\",\"WHILE\"]),AO=v([\"UNION [ALL | DISTINCT]\",\"EXCEPT\",\"INTERSECT\",\"MINUS\"]),sO=v([\"JOIN\",\"{LEFT | RIGHT | FULL} [OUTER] JOIN\",\"{INNER | CROSS} JOIN\",\"NATURAL {LEFT | RIGHT} [OUTER] JOIN\",\"STRAIGHT_JOIN\"]),SO=v([\"ON DELETE\",\"ON UPDATE\",\"CHARACTER SET\",\"{ROWS | RANGE} BETWEEN\",\"IDENTIFIED BY\"]),oO={name:\"singlestoredb\",tokenizerOptions:{reservedSelect:RO,reservedClauses:[...nO,...Cr,...RT],reservedSetOperations:AO,reservedJoins:sO,reservedPhrases:SO,reservedKeywords:tO,reservedDataTypes:TO,reservedFunctionNames:rO,stringTypes:['\"\"-qq-bs',\"''-qq-bs\",{quote:\"''-raw\",prefixes:[\"B\",\"X\"],requirePrefix:!0}],identTypes:[\"``\"],identChars:{first:\"$\",rest:\"$\",allowFirstCharNumber:!0},variableTypes:[{regex:\"@@?[A-Za-z0-9_$]+\"},{quote:\"``\",prefixes:[\"@\"],requirePrefix:!0}],lineCommentTypes:[\"--\",\"#\"],operators:[\":=\",\"&\",\"|\",\"^\",\"~\",\"<<\",\">>\",\"<=>\",\"&&\",\"||\",\"::\",\"::$\",\"::%\",\":>\",\"!:>\",\"*.*\"],postProcess:yt},formatOptions:{alwaysDenseOperators:[\"::\",\"::$\",\"::%\"],onelineClauses:[...Cr,...RT],tabularOnelineClauses:RT}},OO=[\"ABS\",\"ACOS\",\"ACOSH\",\"ADD_MONTHS\",\"ALL_USER_NAMES\",\"ANY_VALUE\",\"APPROX_COUNT_DISTINCT\",\"APPROX_PERCENTILE\",\"APPROX_PERCENTILE_ACCUMULATE\",\"APPROX_PERCENTILE_COMBINE\",\"APPROX_PERCENTILE_ESTIMATE\",\"APPROX_TOP_K\",\"APPROX_TOP_K_ACCUMULATE\",\"APPROX_TOP_K_COMBINE\",\"APPROX_TOP_K_ESTIMATE\",\"APPROXIMATE_JACCARD_INDEX\",\"APPROXIMATE_SIMILARITY\",\"ARRAY_AGG\",\"ARRAY_APPEND\",\"ARRAY_CAT\",\"ARRAY_COMPACT\",\"ARRAY_CONSTRUCT\",\"ARRAY_CONSTRUCT_COMPACT\",\"ARRAY_CONTAINS\",\"ARRAY_INSERT\",\"ARRAY_INTERSECTION\",\"ARRAY_POSITION\",\"ARRAY_PREPEND\",\"ARRAY_SIZE\",\"ARRAY_SLICE\",\"ARRAY_TO_STRING\",\"ARRAY_UNION_AGG\",\"ARRAY_UNIQUE_AGG\",\"ARRAYS_OVERLAP\",\"AS_ARRAY\",\"AS_BINARY\",\"AS_BOOLEAN\",\"AS_CHAR\",\"AS_VARCHAR\",\"AS_DATE\",\"AS_DECIMAL\",\"AS_NUMBER\",\"AS_DOUBLE\",\"AS_REAL\",\"AS_INTEGER\",\"AS_OBJECT\",\"AS_TIME\",\"AS_TIMESTAMP_LTZ\",\"AS_TIMESTAMP_NTZ\",\"AS_TIMESTAMP_TZ\",\"ASCII\",\"ASIN\",\"ASINH\",\"ATAN\",\"ATAN2\",\"ATANH\",\"AUTO_REFRESH_REGISTRATION_HISTORY\",\"AUTOMATIC_CLUSTERING_HISTORY\",\"AVG\",\"BASE64_DECODE_BINARY\",\"BASE64_DECODE_STRING\",\"BASE64_ENCODE\",\"BIT_LENGTH\",\"BITAND\",\"BITAND_AGG\",\"BITMAP_BIT_POSITION\",\"BITMAP_BUCKET_NUMBER\",\"BITMAP_CONSTRUCT_AGG\",\"BITMAP_COUNT\",\"BITMAP_OR_AGG\",\"BITNOT\",\"BITOR\",\"BITOR_AGG\",\"BITSHIFTLEFT\",\"BITSHIFTRIGHT\",\"BITXOR\",\"BITXOR_AGG\",\"BOOLAND\",\"BOOLAND_AGG\",\"BOOLNOT\",\"BOOLOR\",\"BOOLOR_AGG\",\"BOOLXOR\",\"BOOLXOR_AGG\",\"BUILD_SCOPED_FILE_URL\",\"BUILD_STAGE_FILE_URL\",\"CASE\",\"CAST\",\"CBRT\",\"CEIL\",\"CHARINDEX\",\"CHECK_JSON\",\"CHECK_XML\",\"CHR\",\"CHAR\",\"COALESCE\",\"COLLATE\",\"COLLATION\",\"COMPLETE_TASK_GRAPHS\",\"COMPRESS\",\"CONCAT\",\"CONCAT_WS\",\"CONDITIONAL_CHANGE_EVENT\",\"CONDITIONAL_TRUE_EVENT\",\"CONTAINS\",\"CONVERT_TIMEZONE\",\"COPY_HISTORY\",\"CORR\",\"COS\",\"COSH\",\"COT\",\"COUNT\",\"COUNT_IF\",\"COVAR_POP\",\"COVAR_SAMP\",\"CUME_DIST\",\"CURRENT_ACCOUNT\",\"CURRENT_AVAILABLE_ROLES\",\"CURRENT_CLIENT\",\"CURRENT_DATABASE\",\"CURRENT_DATE\",\"CURRENT_IP_ADDRESS\",\"CURRENT_REGION\",\"CURRENT_ROLE\",\"CURRENT_SCHEMA\",\"CURRENT_SCHEMAS\",\"CURRENT_SECONDARY_ROLES\",\"CURRENT_SESSION\",\"CURRENT_STATEMENT\",\"CURRENT_TASK_GRAPHS\",\"CURRENT_TIME\",\"CURRENT_TIMESTAMP\",\"CURRENT_TRANSACTION\",\"CURRENT_USER\",\"CURRENT_VERSION\",\"CURRENT_WAREHOUSE\",\"DATA_TRANSFER_HISTORY\",\"DATABASE_REFRESH_HISTORY\",\"DATABASE_REFRESH_PROGRESS\",\"DATABASE_REFRESH_PROGRESS_BY_JOB\",\"DATABASE_STORAGE_USAGE_HISTORY\",\"DATE_FROM_PARTS\",\"DATE_PART\",\"DATE_TRUNC\",\"DATEADD\",\"DATEDIFF\",\"DAYNAME\",\"DECODE\",\"DECOMPRESS_BINARY\",\"DECOMPRESS_STRING\",\"DECRYPT\",\"DECRYPT_RAW\",\"DEGREES\",\"DENSE_RANK\",\"DIV0\",\"EDITDISTANCE\",\"ENCRYPT\",\"ENCRYPT_RAW\",\"ENDSWITH\",\"EQUAL_NULL\",\"EXP\",\"EXPLAIN_JSON\",\"EXTERNAL_FUNCTIONS_HISTORY\",\"EXTERNAL_TABLE_FILES\",\"EXTERNAL_TABLE_FILE_REGISTRATION_HISTORY\",\"EXTRACT\",\"EXTRACT_SEMANTIC_CATEGORIES\",\"FACTORIAL\",\"FIRST_VALUE\",\"FLATTEN\",\"FLOOR\",\"GENERATE_COLUMN_DESCRIPTION\",\"GENERATOR\",\"GET\",\"GET_ABSOLUTE_PATH\",\"GET_DDL\",\"GET_IGNORE_CASE\",\"GET_OBJECT_REFERENCES\",\"GET_PATH\",\"GET_PRESIGNED_URL\",\"GET_RELATIVE_PATH\",\"GET_STAGE_LOCATION\",\"GETBIT\",\"GREATEST\",\"GROUPING\",\"GROUPING_ID\",\"HASH\",\"HASH_AGG\",\"HAVERSINE\",\"HEX_DECODE_BINARY\",\"HEX_DECODE_STRING\",\"HEX_ENCODE\",\"HLL\",\"HLL_ACCUMULATE\",\"HLL_COMBINE\",\"HLL_ESTIMATE\",\"HLL_EXPORT\",\"HLL_IMPORT\",\"HOUR\",\"MINUTE\",\"SECOND\",\"IFF\",\"IFNULL\",\"ILIKE\",\"ILIKE ANY\",\"INFER_SCHEMA\",\"INITCAP\",\"INSERT\",\"INVOKER_ROLE\",\"INVOKER_SHARE\",\"IS_ARRAY\",\"IS_BINARY\",\"IS_BOOLEAN\",\"IS_CHAR\",\"IS_VARCHAR\",\"IS_DATE\",\"IS_DATE_VALUE\",\"IS_DECIMAL\",\"IS_DOUBLE\",\"IS_REAL\",\"IS_GRANTED_TO_INVOKER_ROLE\",\"IS_INTEGER\",\"IS_NULL_VALUE\",\"IS_OBJECT\",\"IS_ROLE_IN_SESSION\",\"IS_TIME\",\"IS_TIMESTAMP_LTZ\",\"IS_TIMESTAMP_NTZ\",\"IS_TIMESTAMP_TZ\",\"JAROWINKLER_SIMILARITY\",\"JSON_EXTRACT_PATH_TEXT\",\"KURTOSIS\",\"LAG\",\"LAST_DAY\",\"LAST_QUERY_ID\",\"LAST_TRANSACTION\",\"LAST_VALUE\",\"LEAD\",\"LEAST\",\"LEFT\",\"LENGTH\",\"LEN\",\"LIKE\",\"LIKE ALL\",\"LIKE ANY\",\"LISTAGG\",\"LN\",\"LOCALTIME\",\"LOCALTIMESTAMP\",\"LOG\",\"LOGIN_HISTORY\",\"LOGIN_HISTORY_BY_USER\",\"LOWER\",\"LPAD\",\"LTRIM\",\"MATERIALIZED_VIEW_REFRESH_HISTORY\",\"MD5\",\"MD5_HEX\",\"MD5_BINARY\",\"MD5_NUMBER — Obsoleted\",\"MD5_NUMBER_LOWER64\",\"MD5_NUMBER_UPPER64\",\"MEDIAN\",\"MIN\",\"MAX\",\"MINHASH\",\"MINHASH_COMBINE\",\"MOD\",\"MODE\",\"MONTHNAME\",\"MONTHS_BETWEEN\",\"NEXT_DAY\",\"NORMAL\",\"NTH_VALUE\",\"NTILE\",\"NULLIF\",\"NULLIFZERO\",\"NVL\",\"NVL2\",\"OBJECT_AGG\",\"OBJECT_CONSTRUCT\",\"OBJECT_CONSTRUCT_KEEP_NULL\",\"OBJECT_DELETE\",\"OBJECT_INSERT\",\"OBJECT_KEYS\",\"OBJECT_PICK\",\"OCTET_LENGTH\",\"PARSE_IP\",\"PARSE_JSON\",\"PARSE_URL\",\"PARSE_XML\",\"PERCENT_RANK\",\"PERCENTILE_CONT\",\"PERCENTILE_DISC\",\"PI\",\"PIPE_USAGE_HISTORY\",\"POLICY_CONTEXT\",\"POLICY_REFERENCES\",\"POSITION\",\"POW\",\"POWER\",\"PREVIOUS_DAY\",\"QUERY_ACCELERATION_HISTORY\",\"QUERY_HISTORY\",\"QUERY_HISTORY_BY_SESSION\",\"QUERY_HISTORY_BY_USER\",\"QUERY_HISTORY_BY_WAREHOUSE\",\"RADIANS\",\"RANDOM\",\"RANDSTR\",\"RANK\",\"RATIO_TO_REPORT\",\"REGEXP\",\"REGEXP_COUNT\",\"REGEXP_INSTR\",\"REGEXP_LIKE\",\"REGEXP_REPLACE\",\"REGEXP_SUBSTR\",\"REGEXP_SUBSTR_ALL\",\"REGR_AVGX\",\"REGR_AVGY\",\"REGR_COUNT\",\"REGR_INTERCEPT\",\"REGR_R2\",\"REGR_SLOPE\",\"REGR_SXX\",\"REGR_SXY\",\"REGR_SYY\",\"REGR_VALX\",\"REGR_VALY\",\"REPEAT\",\"REPLACE\",\"REPLICATION_GROUP_REFRESH_HISTORY\",\"REPLICATION_GROUP_REFRESH_PROGRESS\",\"REPLICATION_GROUP_REFRESH_PROGRESS_BY_JOB\",\"REPLICATION_GROUP_USAGE_HISTORY\",\"REPLICATION_USAGE_HISTORY\",\"REST_EVENT_HISTORY\",\"RESULT_SCAN\",\"REVERSE\",\"RIGHT\",\"RLIKE\",\"ROUND\",\"ROW_NUMBER\",\"RPAD\",\"RTRIM\",\"RTRIMMED_LENGTH\",\"SEARCH_OPTIMIZATION_HISTORY\",\"SEQ1\",\"SEQ2\",\"SEQ4\",\"SEQ8\",\"SERVERLESS_TASK_HISTORY\",\"SHA1\",\"SHA1_HEX\",\"SHA1_BINARY\",\"SHA2\",\"SHA2_HEX\",\"SHA2_BINARY\",\"SIGN\",\"SIN\",\"SINH\",\"SKEW\",\"SOUNDEX\",\"SPACE\",\"SPLIT\",\"SPLIT_PART\",\"SPLIT_TO_TABLE\",\"SQRT\",\"SQUARE\",\"ST_AREA\",\"ST_ASEWKB\",\"ST_ASEWKT\",\"ST_ASGEOJSON\",\"ST_ASWKB\",\"ST_ASBINARY\",\"ST_ASWKT\",\"ST_ASTEXT\",\"ST_AZIMUTH\",\"ST_CENTROID\",\"ST_COLLECT\",\"ST_CONTAINS\",\"ST_COVEREDBY\",\"ST_COVERS\",\"ST_DIFFERENCE\",\"ST_DIMENSION\",\"ST_DISJOINT\",\"ST_DISTANCE\",\"ST_DWITHIN\",\"ST_ENDPOINT\",\"ST_ENVELOPE\",\"ST_GEOGFROMGEOHASH\",\"ST_GEOGPOINTFROMGEOHASH\",\"ST_GEOGRAPHYFROMWKB\",\"ST_GEOGRAPHYFROMWKT\",\"ST_GEOHASH\",\"ST_GEOMETRYFROMWKB\",\"ST_GEOMETRYFROMWKT\",\"ST_HAUSDORFFDISTANCE\",\"ST_INTERSECTION\",\"ST_INTERSECTS\",\"ST_LENGTH\",\"ST_MAKEGEOMPOINT\",\"ST_GEOM_POINT\",\"ST_MAKELINE\",\"ST_MAKEPOINT\",\"ST_POINT\",\"ST_MAKEPOLYGON\",\"ST_POLYGON\",\"ST_NPOINTS\",\"ST_NUMPOINTS\",\"ST_PERIMETER\",\"ST_POINTN\",\"ST_SETSRID\",\"ST_SIMPLIFY\",\"ST_SRID\",\"ST_STARTPOINT\",\"ST_SYMDIFFERENCE\",\"ST_UNION\",\"ST_WITHIN\",\"ST_X\",\"ST_XMAX\",\"ST_XMIN\",\"ST_Y\",\"ST_YMAX\",\"ST_YMIN\",\"STAGE_DIRECTORY_FILE_REGISTRATION_HISTORY\",\"STAGE_STORAGE_USAGE_HISTORY\",\"STARTSWITH\",\"STDDEV\",\"STDDEV_POP\",\"STDDEV_SAMP\",\"STRIP_NULL_VALUE\",\"STRTOK\",\"STRTOK_SPLIT_TO_TABLE\",\"STRTOK_TO_ARRAY\",\"SUBSTR\",\"SUBSTRING\",\"SUM\",\"SYSDATE\",\"SYSTEM$ABORT_SESSION\",\"SYSTEM$ABORT_TRANSACTION\",\"SYSTEM$AUTHORIZE_PRIVATELINK\",\"SYSTEM$AUTHORIZE_STAGE_PRIVATELINK_ACCESS\",\"SYSTEM$BEHAVIOR_CHANGE_BUNDLE_STATUS\",\"SYSTEM$CANCEL_ALL_QUERIES\",\"SYSTEM$CANCEL_QUERY\",\"SYSTEM$CLUSTERING_DEPTH\",\"SYSTEM$CLUSTERING_INFORMATION\",\"SYSTEM$CLUSTERING_RATIO \",\"SYSTEM$CURRENT_USER_TASK_NAME\",\"SYSTEM$DATABASE_REFRESH_HISTORY \",\"SYSTEM$DATABASE_REFRESH_PROGRESS\",\"SYSTEM$DATABASE_REFRESH_PROGRESS_BY_JOB \",\"SYSTEM$DISABLE_BEHAVIOR_CHANGE_BUNDLE\",\"SYSTEM$DISABLE_DATABASE_REPLICATION\",\"SYSTEM$ENABLE_BEHAVIOR_CHANGE_BUNDLE\",\"SYSTEM$ESTIMATE_QUERY_ACCELERATION\",\"SYSTEM$ESTIMATE_SEARCH_OPTIMIZATION_COSTS\",\"SYSTEM$EXPLAIN_JSON_TO_TEXT\",\"SYSTEM$EXPLAIN_PLAN_JSON\",\"SYSTEM$EXTERNAL_TABLE_PIPE_STATUS\",\"SYSTEM$GENERATE_SAML_CSR\",\"SYSTEM$GENERATE_SCIM_ACCESS_TOKEN\",\"SYSTEM$GET_AWS_SNS_IAM_POLICY\",\"SYSTEM$GET_PREDECESSOR_RETURN_VALUE\",\"SYSTEM$GET_PRIVATELINK\",\"SYSTEM$GET_PRIVATELINK_AUTHORIZED_ENDPOINTS\",\"SYSTEM$GET_PRIVATELINK_CONFIG\",\"SYSTEM$GET_SNOWFLAKE_PLATFORM_INFO\",\"SYSTEM$GET_TAG\",\"SYSTEM$GET_TAG_ALLOWED_VALUES\",\"SYSTEM$GET_TAG_ON_CURRENT_COLUMN\",\"SYSTEM$GET_TAG_ON_CURRENT_TABLE\",\"SYSTEM$GLOBAL_ACCOUNT_SET_PARAMETER\",\"SYSTEM$LAST_CHANGE_COMMIT_TIME\",\"SYSTEM$LINK_ACCOUNT_OBJECTS_BY_NAME\",\"SYSTEM$MIGRATE_SAML_IDP_REGISTRATION\",\"SYSTEM$PIPE_FORCE_RESUME\",\"SYSTEM$PIPE_STATUS\",\"SYSTEM$REVOKE_PRIVATELINK\",\"SYSTEM$REVOKE_STAGE_PRIVATELINK_ACCESS\",\"SYSTEM$SET_RETURN_VALUE\",\"SYSTEM$SHOW_OAUTH_CLIENT_SECRETS\",\"SYSTEM$STREAM_GET_TABLE_TIMESTAMP\",\"SYSTEM$STREAM_HAS_DATA\",\"SYSTEM$TASK_DEPENDENTS_ENABLE\",\"SYSTEM$TYPEOF\",\"SYSTEM$USER_TASK_CANCEL_ONGOING_EXECUTIONS\",\"SYSTEM$VERIFY_EXTERNAL_OAUTH_TOKEN\",\"SYSTEM$WAIT\",\"SYSTEM$WHITELIST\",\"SYSTEM$WHITELIST_PRIVATELINK\",\"TAG_REFERENCES\",\"TAG_REFERENCES_ALL_COLUMNS\",\"TAG_REFERENCES_WITH_LINEAGE\",\"TAN\",\"TANH\",\"TASK_DEPENDENTS\",\"TASK_HISTORY\",\"TIME_FROM_PARTS\",\"TIME_SLICE\",\"TIMEADD\",\"TIMEDIFF\",\"TIMESTAMP_FROM_PARTS\",\"TIMESTAMPADD\",\"TIMESTAMPDIFF\",\"TO_ARRAY\",\"TO_BINARY\",\"TO_BOOLEAN\",\"TO_CHAR\",\"TO_VARCHAR\",\"TO_DATE\",\"DATE\",\"TO_DECIMAL\",\"TO_NUMBER\",\"TO_NUMERIC\",\"TO_DOUBLE\",\"TO_GEOGRAPHY\",\"TO_GEOMETRY\",\"TO_JSON\",\"TO_OBJECT\",\"TO_TIME\",\"TIME\",\"TO_TIMESTAMP\",\"TO_TIMESTAMP_LTZ\",\"TO_TIMESTAMP_NTZ\",\"TO_TIMESTAMP_TZ\",\"TO_VARIANT\",\"TO_XML\",\"TRANSLATE\",\"TRIM\",\"TRUNCATE\",\"TRUNC\",\"TRUNC\",\"TRY_BASE64_DECODE_BINARY\",\"TRY_BASE64_DECODE_STRING\",\"TRY_CAST\",\"TRY_HEX_DECODE_BINARY\",\"TRY_HEX_DECODE_STRING\",\"TRY_PARSE_JSON\",\"TRY_TO_BINARY\",\"TRY_TO_BOOLEAN\",\"TRY_TO_DATE\",\"TRY_TO_DECIMAL\",\"TRY_TO_NUMBER\",\"TRY_TO_NUMERIC\",\"TRY_TO_DOUBLE\",\"TRY_TO_GEOGRAPHY\",\"TRY_TO_GEOMETRY\",\"TRY_TO_TIME\",\"TRY_TO_TIMESTAMP\",\"TRY_TO_TIMESTAMP_LTZ\",\"TRY_TO_TIMESTAMP_NTZ\",\"TRY_TO_TIMESTAMP_TZ\",\"TYPEOF\",\"UNICODE\",\"UNIFORM\",\"UPPER\",\"UUID_STRING\",\"VALIDATE\",\"VALIDATE_PIPE_LOAD\",\"VAR_POP\",\"VAR_SAMP\",\"VARIANCE\",\"VARIANCE_SAMP\",\"VARIANCE_POP\",\"WAREHOUSE_LOAD_HISTORY\",\"WAREHOUSE_METERING_HISTORY\",\"WIDTH_BUCKET\",\"XMLGET\",\"YEAR\",\"YEAROFWEEK\",\"YEAROFWEEKISO\",\"DAY\",\"DAYOFMONTH\",\"DAYOFWEEK\",\"DAYOFWEEKISO\",\"DAYOFYEAR\",\"WEEK\",\"WEEK\",\"WEEKOFYEAR\",\"WEEKISO\",\"MONTH\",\"QUARTER\",\"ZEROIFNULL\",\"ZIPF\"],iO=[\"ACCOUNT\",\"ALL\",\"ALTER\",\"AND\",\"ANY\",\"AS\",\"BETWEEN\",\"BY\",\"CASE\",\"CAST\",\"CHECK\",\"COLUMN\",\"CONNECT\",\"CONNECTION\",\"CONSTRAINT\",\"CREATE\",\"CROSS\",\"CURRENT\",\"CURRENT_DATE\",\"CURRENT_TIME\",\"CURRENT_TIMESTAMP\",\"CURRENT_USER\",\"DATABASE\",\"DELETE\",\"DISTINCT\",\"DROP\",\"ELSE\",\"EXISTS\",\"FALSE\",\"FOLLOWING\",\"FOR\",\"FROM\",\"FULL\",\"GRANT\",\"GROUP\",\"GSCLUSTER\",\"HAVING\",\"ILIKE\",\"IN\",\"INCREMENT\",\"INNER\",\"INSERT\",\"INTERSECT\",\"INTO\",\"IS\",\"ISSUE\",\"JOIN\",\"LATERAL\",\"LEFT\",\"LIKE\",\"LOCALTIME\",\"LOCALTIMESTAMP\",\"MINUS\",\"NATURAL\",\"NOT\",\"NULL\",\"OF\",\"ON\",\"OR\",\"ORDER\",\"ORGANIZATION\",\"QUALIFY\",\"REGEXP\",\"REVOKE\",\"RIGHT\",\"RLIKE\",\"ROW\",\"ROWS\",\"SAMPLE\",\"SCHEMA\",\"SELECT\",\"SET\",\"SOME\",\"START\",\"TABLE\",\"TABLESAMPLE\",\"THEN\",\"TO\",\"TRIGGER\",\"TRUE\",\"TRY_CAST\",\"UNION\",\"UNIQUE\",\"UPDATE\",\"USING\",\"VALUES\",\"VIEW\",\"WHEN\",\"WHENEVER\",\"WHERE\",\"WITH\",\"COMMENT\"],aO=[\"NUMBER\",\"DECIMAL\",\"NUMERIC\",\"INT\",\"INTEGER\",\"BIGINT\",\"SMALLINT\",\"TINYINT\",\"BYTEINT\",\"FLOAT\",\"FLOAT4\",\"FLOAT8\",\"DOUBLE\",\"DOUBLE PRECISION\",\"REAL\",\"VARCHAR\",\"CHAR\",\"CHARACTER\",\"STRING\",\"TEXT\",\"BINARY\",\"VARBINARY\",\"BOOLEAN\",\"DATE\",\"DATETIME\",\"TIME\",\"TIMESTAMP\",\"TIMESTAMP_LTZ\",\"TIMESTAMP_NTZ\",\"TIMESTAMP\",\"TIMESTAMP_TZ\",\"VARIANT\",\"OBJECT\",\"ARRAY\",\"GEOGRAPHY\",\"GEOMETRY\"],IO=v([\"SELECT [ALL | DISTINCT]\"]),NO=v([\"WITH [RECURSIVE]\",\"FROM\",\"WHERE\",\"GROUP BY\",\"HAVING\",\"PARTITION BY\",\"ORDER BY\",\"QUALIFY\",\"LIMIT\",\"OFFSET\",\"FETCH [FIRST | NEXT]\",\"INSERT [OVERWRITE] [ALL INTO | INTO | ALL | FIRST]\",\"{THEN | ELSE} INTO\",\"VALUES\",\"SET\",\"CLUSTER BY\",\"[WITH] {MASKING POLICY | TAG | ROW ACCESS POLICY}\",\"COPY GRANTS\",\"USING TEMPLATE\",\"MERGE INTO\",\"WHEN MATCHED [AND]\",\"THEN {UPDATE SET | DELETE}\",\"WHEN NOT MATCHED THEN INSERT\"]),ur=v([\"CREATE [OR REPLACE] [VOLATILE] TABLE [IF NOT EXISTS]\",\"CREATE [OR REPLACE] [LOCAL | GLOBAL] {TEMP|TEMPORARY} TABLE [IF NOT EXISTS]\"]),nT=v([\"CREATE [OR REPLACE] [SECURE] [RECURSIVE] VIEW [IF NOT EXISTS]\",\"UPDATE\",\"DELETE FROM\",\"DROP TABLE [IF EXISTS]\",\"ALTER TABLE [IF EXISTS]\",\"RENAME TO\",\"SWAP WITH\",\"[SUSPEND | RESUME] RECLUSTER\",\"DROP CLUSTERING KEY\",\"ADD [COLUMN]\",\"RENAME COLUMN\",\"{ALTER | MODIFY} [COLUMN]\",\"DROP [COLUMN]\",\"{ADD | ALTER | MODIFY | DROP} [CONSTRAINT]\",\"RENAME CONSTRAINT\",\"{ADD | DROP} SEARCH OPTIMIZATION\",\"{SET | UNSET} TAG\",\"{ADD | DROP} ROW ACCESS POLICY\",\"DROP ALL ROW ACCESS POLICIES\",\"{SET | DROP} DEFAULT\",\"{SET | DROP} NOT NULL\",\"[SET DATA] TYPE\",\"UNSET COMMENT\",\"{SET | UNSET} MASKING POLICY\",\"TRUNCATE [TABLE] [IF EXISTS]\",\"ALTER ACCOUNT\",\"ALTER API INTEGRATION\",\"ALTER CONNECTION\",\"ALTER DATABASE\",\"ALTER EXTERNAL TABLE\",\"ALTER FAILOVER GROUP\",\"ALTER FILE FORMAT\",\"ALTER FUNCTION\",\"ALTER INTEGRATION\",\"ALTER MASKING POLICY\",\"ALTER MATERIALIZED VIEW\",\"ALTER NETWORK POLICY\",\"ALTER NOTIFICATION INTEGRATION\",\"ALTER PIPE\",\"ALTER PROCEDURE\",\"ALTER REPLICATION GROUP\",\"ALTER RESOURCE MONITOR\",\"ALTER ROLE\",\"ALTER ROW ACCESS POLICY\",\"ALTER SCHEMA\",\"ALTER SECURITY INTEGRATION\",\"ALTER SEQUENCE\",\"ALTER SESSION\",\"ALTER SESSION POLICY\",\"ALTER SHARE\",\"ALTER STAGE\",\"ALTER STORAGE INTEGRATION\",\"ALTER STREAM\",\"ALTER TAG\",\"ALTER TASK\",\"ALTER USER\",\"ALTER VIEW\",\"ALTER WAREHOUSE\",\"BEGIN\",\"CALL\",\"COMMIT\",\"COPY INTO\",\"CREATE ACCOUNT\",\"CREATE API INTEGRATION\",\"CREATE CONNECTION\",\"CREATE DATABASE\",\"CREATE EXTERNAL FUNCTION\",\"CREATE EXTERNAL TABLE\",\"CREATE FAILOVER GROUP\",\"CREATE FILE FORMAT\",\"CREATE FUNCTION\",\"CREATE INTEGRATION\",\"CREATE MANAGED ACCOUNT\",\"CREATE MASKING POLICY\",\"CREATE MATERIALIZED VIEW\",\"CREATE NETWORK POLICY\",\"CREATE NOTIFICATION INTEGRATION\",\"CREATE PIPE\",\"CREATE PROCEDURE\",\"CREATE REPLICATION GROUP\",\"CREATE RESOURCE MONITOR\",\"CREATE ROLE\",\"CREATE ROW ACCESS POLICY\",\"CREATE SCHEMA\",\"CREATE SECURITY INTEGRATION\",\"CREATE SEQUENCE\",\"CREATE SESSION POLICY\",\"CREATE SHARE\",\"CREATE STAGE\",\"CREATE STORAGE INTEGRATION\",\"CREATE STREAM\",\"CREATE TAG\",\"CREATE TASK\",\"CREATE USER\",\"CREATE WAREHOUSE\",\"DELETE\",\"DESCRIBE DATABASE\",\"DESCRIBE EXTERNAL TABLE\",\"DESCRIBE FILE FORMAT\",\"DESCRIBE FUNCTION\",\"DESCRIBE INTEGRATION\",\"DESCRIBE MASKING POLICY\",\"DESCRIBE MATERIALIZED VIEW\",\"DESCRIBE NETWORK POLICY\",\"DESCRIBE PIPE\",\"DESCRIBE PROCEDURE\",\"DESCRIBE RESULT\",\"DESCRIBE ROW ACCESS POLICY\",\"DESCRIBE SCHEMA\",\"DESCRIBE SEQUENCE\",\"DESCRIBE SESSION POLICY\",\"DESCRIBE SHARE\",\"DESCRIBE STAGE\",\"DESCRIBE STREAM\",\"DESCRIBE TABLE\",\"DESCRIBE TASK\",\"DESCRIBE TRANSACTION\",\"DESCRIBE USER\",\"DESCRIBE VIEW\",\"DESCRIBE WAREHOUSE\",\"DROP CONNECTION\",\"DROP DATABASE\",\"DROP EXTERNAL TABLE\",\"DROP FAILOVER GROUP\",\"DROP FILE FORMAT\",\"DROP FUNCTION\",\"DROP INTEGRATION\",\"DROP MANAGED ACCOUNT\",\"DROP MASKING POLICY\",\"DROP MATERIALIZED VIEW\",\"DROP NETWORK POLICY\",\"DROP PIPE\",\"DROP PROCEDURE\",\"DROP REPLICATION GROUP\",\"DROP RESOURCE MONITOR\",\"DROP ROLE\",\"DROP ROW ACCESS POLICY\",\"DROP SCHEMA\",\"DROP SEQUENCE\",\"DROP SESSION POLICY\",\"DROP SHARE\",\"DROP STAGE\",\"DROP STREAM\",\"DROP TAG\",\"DROP TASK\",\"DROP USER\",\"DROP VIEW\",\"DROP WAREHOUSE\",\"EXECUTE IMMEDIATE\",\"EXECUTE TASK\",\"EXPLAIN\",\"GET\",\"GRANT OWNERSHIP\",\"GRANT ROLE\",\"INSERT\",\"LIST\",\"MERGE\",\"PUT\",\"REMOVE\",\"REVOKE ROLE\",\"ROLLBACK\",\"SHOW COLUMNS\",\"SHOW CONNECTIONS\",\"SHOW DATABASES\",\"SHOW DATABASES IN FAILOVER GROUP\",\"SHOW DATABASES IN REPLICATION GROUP\",\"SHOW DELEGATED AUTHORIZATIONS\",\"SHOW EXTERNAL FUNCTIONS\",\"SHOW EXTERNAL TABLES\",\"SHOW FAILOVER GROUPS\",\"SHOW FILE FORMATS\",\"SHOW FUNCTIONS\",\"SHOW GLOBAL ACCOUNTS\",\"SHOW GRANTS\",\"SHOW INTEGRATIONS\",\"SHOW LOCKS\",\"SHOW MANAGED ACCOUNTS\",\"SHOW MASKING POLICIES\",\"SHOW MATERIALIZED VIEWS\",\"SHOW NETWORK POLICIES\",\"SHOW OBJECTS\",\"SHOW ORGANIZATION ACCOUNTS\",\"SHOW PARAMETERS\",\"SHOW PIPES\",\"SHOW PRIMARY KEYS\",\"SHOW PROCEDURES\",\"SHOW REGIONS\",\"SHOW REPLICATION ACCOUNTS\",\"SHOW REPLICATION DATABASES\",\"SHOW REPLICATION GROUPS\",\"SHOW RESOURCE MONITORS\",\"SHOW ROLES\",\"SHOW ROW ACCESS POLICIES\",\"SHOW SCHEMAS\",\"SHOW SEQUENCES\",\"SHOW SESSION POLICIES\",\"SHOW SHARES\",\"SHOW SHARES IN FAILOVER GROUP\",\"SHOW SHARES IN REPLICATION GROUP\",\"SHOW STAGES\",\"SHOW STREAMS\",\"SHOW TABLES\",\"SHOW TAGS\",\"SHOW TASKS\",\"SHOW TRANSACTIONS\",\"SHOW USER FUNCTIONS\",\"SHOW USERS\",\"SHOW VARIABLES\",\"SHOW VIEWS\",\"SHOW WAREHOUSES\",\"TRUNCATE MATERIALIZED VIEW\",\"UNDROP DATABASE\",\"UNDROP SCHEMA\",\"UNDROP TABLE\",\"UNDROP TAG\",\"UNSET\",\"USE DATABASE\",\"USE ROLE\",\"USE SCHEMA\",\"USE SECONDARY ROLES\",\"USE WAREHOUSE\"]),lO=v([\"UNION [ALL]\",\"MINUS\",\"EXCEPT\",\"INTERSECT\"]),_O=v([\"[INNER] JOIN\",\"[NATURAL] {LEFT | RIGHT | FULL} [OUTER] JOIN\",\"{CROSS | NATURAL} JOIN\"]),LO=v([\"{ROWS | RANGE} BETWEEN\",\"ON {UPDATE | DELETE} [SET NULL | SET DEFAULT]\"]),CO={name:\"snowflake\",tokenizerOptions:{reservedSelect:IO,reservedClauses:[...NO,...ur,...nT],reservedSetOperations:lO,reservedJoins:_O,reservedPhrases:LO,reservedKeywords:iO,reservedDataTypes:aO,reservedFunctionNames:OO,stringTypes:[\"$$\",\"''-qq-bs\"],identTypes:['\"\"-qq'],variableTypes:[{regex:\"[$][1-9]\\\\\\\\d*\"},{regex:\"[$][_a-zA-Z][_a-zA-Z0-9$]*\"}],extraParens:[\"[]\"],identChars:{rest:\"$\"},lineCommentTypes:[\"--\",\"//\"],operators:[\"%\",\"::\",\"||\",\"=>\"],propertyAccessOperators:[\":\"]},formatOptions:{alwaysDenseOperators:[\"::\"],onelineClauses:[...ur,...nT],tabularOnelineClauses:nT}},ot=E=>E[E.length-1],$R=E=>E.sort((e,T)=>T.length-e.length||e.localeCompare(T)),Dt=E=>E.replace(/\\\\s+/gu,\" \"),AT=E=>/\\\\n/.test(E),CE=E=>E.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/gu,\"\\\\\\\\$&\"),cr=/\\\\s+/uy,wE=E=>new RegExp(`(?:${E})`,\"uy\"),uO=E=>E.split(\"\").map(e=>/ /gu.test(e)?\"\\\\\\\\s+\":`[${e.toUpperCase()}${e.toLowerCase()}]`).join(\"\"),cO=E=>E+\"(?:-\"+E+\")*\",fO=({prefixes:E,requirePrefix:e})=>`(?:${E.map(uO).join(\"|\")}${e?\"\":\"|\"})`,PO=E=>new RegExp(`(?:${E.map(CE).join(\"|\")}).*?(?=\\\\r\n|\\\\r|\n|$)`,\"uy\"),fr=(E,e=[])=>{const T=E===\"open\"?0:1,t=[\"()\",...e].map(r=>r[T]);return wE(t.map(CE).join(\"|\"))},Pr=E=>wE(`${$R(E).map(CE).join(\"|\")}`),DO=({rest:E,dashes:e})=>E||e?`(?![${E||\"\"}${e?\"-\":\"\"}])`:\"\",hE=(E,e={})=>{if(E.length===0)return/^\\\\b$/u;const T=DO(e),t=$R(E).map(CE).join(\"|\").replace(/ /gu,\"\\\\\\\\s+\");return new RegExp(`(?:${t})${T}\\\\\\\\b`,\"iuy\")},sT=(E,e)=>{if(!E.length)return;const T=E.map(CE).join(\"|\");return wE(`(?:${T})(?:${e})`)},dO=()=>{const E={\"<\":\">\",\"[\":\"]\",\"(\":\")\",\"{\":\"}\"},e=\"{left}(?:(?!{right}').)*?{right}\",T=Object.entries(E).map(([n,s])=>e.replace(/{left}/g,CE(n)).replace(/{right}/g,CE(s))),t=CE(Object.keys(E).join(\"\"));return`[Qq]'(?:${String.raw`(?<tag>[^\\\\s${t}])(?:(?!\\\\k<tag>').)*?\\\\k<tag>`}|${T.join(\"|\")})'`},Dr={\"``\":\"(?:`[^`]*`)+\",\"[]\":String.raw`(?:\\\\[[^\\\\]]*\\\\])(?:\\\\][^\\\\]]*\\\\])*`,'\"\"-qq':String.raw`(?:\"[^\"]*\")+`,'\"\"-bs':String.raw`(?:\"[^\"\\\\\\\\]*(?:\\\\\\\\.[^\"\\\\\\\\]*)*\")`,'\"\"-qq-bs':String.raw`(?:\"[^\"\\\\\\\\]*(?:\\\\\\\\.[^\"\\\\\\\\]*)*\")+`,'\"\"-raw':String.raw`(?:\"[^\"]*\")`,\"''-qq\":String.raw`(?:'[^']*')+`,\"''-bs\":String.raw`(?:'[^'\\\\\\\\]*(?:\\\\\\\\.[^'\\\\\\\\]*)*')`,\"''-qq-bs\":String.raw`(?:'[^'\\\\\\\\]*(?:\\\\\\\\.[^'\\\\\\\\]*)*')+`,\"''-raw\":String.raw`(?:'[^']*')`,$$:String.raw`(?<tag>\\\\$\\\\w*\\\\$)[\\\\s\\\\S]*?\\\\k<tag>`,\"\\'\\'\\'..\\'\\'\\'\":String.raw`\\'\\'\\'[^\\\\\\\\]*?(?:\\\\\\\\.[^\\\\\\\\]*?)*?\\'\\'\\'`,'\"\"\"..\"\"\"':String.raw`\"\"\"[^\\\\\\\\]*?(?:\\\\\\\\.[^\\\\\\\\]*?)*?\"\"\"`,\"{}\":String.raw`(?:\\\\{[^\\\\}]*\\\\})`,\"q''\":dO()},xR=E=>typeof E==\"string\"?Dr[E]:\"regex\"in E?E.regex:fO(E)+Dr[E.quote],pO=E=>wE(E.map(e=>\"regex\"in e?e.regex:xR(e)).join(\"|\")),XR=E=>E.map(xR).join(\"|\"),dr=E=>wE(XR(E)),MO=(E={})=>wE(kR(E)),kR=({first:E,rest:e,dashes:T,allowFirstCharNumber:t}={})=>{const r=\"\\\\\\\\p{Alphabetic}\\\\\\\\p{Mark}_\",R=\"\\\\\\\\p{Decimal_Number}\",n=CE(E??\"\"),s=CE(e??\"\"),S=t?`[${r}${R}${n}][${r}${R}${s}]*`:`[${r}${n}][${r}${R}${s}]*`;return T?cO(S):S};function KR(E,e){const T=E.slice(0,e).split(/\\\\n/);return{line:T.length,col:T[T.length-1].length+1}}var UO=class{constructor(E,e){this.rules=E,this.dialectName=e,this.input=\"\",this.index=0}tokenize(E){this.input=E,this.index=0;const e=[];let T;for(;this.index<this.input.length;){const t=this.getWhitespace();if(this.index<this.input.length){if(T=this.getNextToken(),!T)throw this.createParseError();e.push(AE(EE({},T),{precedingWhitespace:t}))}}return e}createParseError(){const E=this.input.slice(this.index,this.index+10),{line:e,col:T}=KR(this.input,this.index);return new Error(`Parse error: Unexpected \"${E}\" at line ${e} column ${T}.\n${this.dialectInfo()}`)}dialectInfo(){return this.dialectName===\"sql\"?`This likely happens because you're using the default \"sql\" dialect.\nIf possible, please select a more specific dialect (like sqlite, postgresql, etc).`:`SQL dialect used: \"${this.dialectName}\".`}getWhitespace(){cr.lastIndex=this.index;const E=cr.exec(this.input);if(E)return this.index+=E[0].length,E[0]}getNextToken(){for(const E of this.rules){const e=this.match(E);if(e)return e}}match(E){E.regex.lastIndex=this.index;const e=E.regex.exec(this.input);if(e){const T=e[0],t={type:E.type,raw:T,text:E.text?E.text(T):T,start:this.index};return E.key&&(t.key=E.key(T)),this.index+=T.length,t}}},pr=/\\\\/\\\\*/uy,mO=/([^/*]|\\\\*[^/]|\\\\/[^*])+/uy,hO=/\\\\*\\\\//uy,GO=class{constructor(){this.lastIndex=0}exec(E){let e=\"\",T,t=0;if(T=this.matchSection(pr,E))e+=T,t++;else return null;for(;t>0;)if(T=this.matchSection(pr,E))e+=T,t++;else if(T=this.matchSection(hO,E))e+=T,t--;else if(T=this.matchSection(mO,E))e+=T;else return null;return[e]}matchSection(E,e){E.lastIndex=this.lastIndex;const T=E.exec(e);return T&&(this.lastIndex+=T[0].length),T?T[0]:null}},gO=class{constructor(E,e){this.cfg=E,this.dialectName=e,this.rulesBeforeParams=this.buildRulesBeforeParams(E),this.rulesAfterParams=this.buildRulesAfterParams(E)}tokenize(E,e){const T=[...this.rulesBeforeParams,...this.buildParamRules(this.cfg,e),...this.rulesAfterParams],t=new UO(T,this.dialectName).tokenize(E);return this.cfg.postProcess?this.cfg.postProcess(t):t}buildRulesBeforeParams(E){var e,T;return this.validRules([{type:\"BLOCK_COMMENT\",regex:/(\\\\/\\\\* *sql-formatter-disable *\\\\*\\\\/[\\\\s\\\\S]*?(?:\\\\/\\\\* *sql-formatter-enable *\\\\*\\\\/|$))/uy},{type:\"BLOCK_COMMENT\",regex:E.nestedBlockComments?new GO:/(\\\\/\\\\*[^]*?\\\\*\\\\/)/uy},{type:\"LINE_COMMENT\",regex:PO((e=E.lineCommentTypes)!=null?e:[\"--\"])},{type:\"QUOTED_IDENTIFIER\",regex:dr(E.identTypes)},{type:\"NUMBER\",regex:/(?:0x[0-9a-fA-F]+|0b[01]+|(?:-\\\\s*)?[0-9]+(?:\\\\.[0-9]*)?(?:[eE][-+]?[0-9]+(?:\\\\.[0-9]+)?)?)(?![\\\\w\\\\p{Alphabetic}])/uy},{type:\"RESERVED_PHRASE\",regex:hE((T=E.reservedPhrases)!=null?T:[],E.identChars),text:Je},{type:\"CASE\",regex:/CASE\\\\b/iuy,text:Je},{type:\"END\",regex:/END\\\\b/iuy,text:Je},{type:\"BETWEEN\",regex:/BETWEEN\\\\b/iuy,text:Je},{type:\"LIMIT\",regex:E.reservedClauses.includes(\"LIMIT\")?/LIMIT\\\\b/iuy:void 0,text:Je},{type:\"RESERVED_CLAUSE\",regex:hE(E.reservedClauses,E.identChars),text:Je},{type:\"RESERVED_SELECT\",regex:hE(E.reservedSelect,E.identChars),text:Je},{type:\"RESERVED_SET_OPERATION\",regex:hE(E.reservedSetOperations,E.identChars),text:Je},{type:\"WHEN\",regex:/WHEN\\\\b/iuy,text:Je},{type:\"ELSE\",regex:/ELSE\\\\b/iuy,text:Je},{type:\"THEN\",regex:/THEN\\\\b/iuy,text:Je},{type:\"RESERVED_JOIN\",regex:hE(E.reservedJoins,E.identChars),text:Je},{type:\"AND\",regex:/AND\\\\b/iuy,text:Je},{type:\"OR\",regex:/OR\\\\b/iuy,text:Je},{type:\"XOR\",regex:E.supportsXor?/XOR\\\\b/iuy:void 0,text:Je},{type:\"RESERVED_FUNCTION_NAME\",regex:hE(E.reservedFunctionNames,E.identChars),text:Je},{type:\"RESERVED_DATA_TYPE\",regex:hE(E.reservedDataTypes,E.identChars),text:Je},{type:\"RESERVED_KEYWORD\",regex:hE(E.reservedKeywords,E.identChars),text:Je}])}buildRulesAfterParams(E){var e,T;return this.validRules([{type:\"VARIABLE\",regex:E.variableTypes?pO(E.variableTypes):void 0},{type:\"STRING\",regex:dr(E.stringTypes)},{type:\"IDENTIFIER\",regex:MO(E.identChars)},{type:\"DELIMITER\",regex:/[;]/uy},{type:\"COMMA\",regex:/[,]/y},{type:\"OPEN_PAREN\",regex:fr(\"open\",E.extraParens)},{type:\"CLOSE_PAREN\",regex:fr(\"close\",E.extraParens)},{type:\"OPERATOR\",regex:Pr([\"+\",\"-\",\"/\",\">\",\"<\",\"=\",\"<>\",\"<=\",\">=\",\"!=\",...(e=E.operators)!=null?e:[]])},{type:\"ASTERISK\",regex:/[*]/uy},{type:\"PROPERTY_ACCESS_OPERATOR\",regex:Pr([\".\",...(T=E.propertyAccessOperators)!=null?T:[]])}])}buildParamRules(E,e){var T,t,r,R,n;const s={named:(e==null?void 0:e.named)||((T=E.paramTypes)==null?void 0:T.named)||[],quoted:(e==null?void 0:e.quoted)||((t=E.paramTypes)==null?void 0:t.quoted)||[],numbered:(e==null?void 0:e.numbered)||((r=E.paramTypes)==null?void 0:r.numbered)||[],positional:typeof(e==null?void 0:e.positional)==\"boolean\"?e.positional:(R=E.paramTypes)==null?void 0:R.positional,custom:(e==null?void 0:e.custom)||((n=E.paramTypes)==null?void 0:n.custom)||[]};return this.validRules([{type:\"NAMED_PARAMETER\",regex:sT(s.named,kR(E.paramChars||E.identChars)),key:S=>S.slice(1)},{type:\"QUOTED_PARAMETER\",regex:sT(s.quoted,XR(E.identTypes)),key:S=>(({tokenKey:A,quoteChar:o})=>A.replace(new RegExp(CE(\"\\\\\\\\\"+o),\"gu\"),o))({tokenKey:S.slice(2,-1),quoteChar:S.slice(-1)})},{type:\"NUMBERED_PARAMETER\",regex:sT(s.numbered,\"[0-9]+\"),key:S=>S.slice(1)},{type:\"POSITIONAL_PARAMETER\",regex:s.positional?/[?]/y:void 0},...s.custom.map(S=>{var A;return{type:\"CUSTOM_PARAMETER\",regex:wE(S.regex),key:(A=S.key)!=null?A:o=>o}})])}validRules(E){return E.filter(e=>!!e.regex)}},Je=E=>Dt(E.toUpperCase()),Mr=new Map,HO=E=>{let e=Mr.get(E);return e||(e=bO(E),Mr.set(E,e)),e},bO=E=>({tokenizer:new gO(E.tokenizerOptions,E.name),formatOptions:yO(E.formatOptions)}),yO=E=>{var e;return{alwaysDenseOperators:E.alwaysDenseOperators||[],onelineClauses:Object.fromEntries(E.onelineClauses.map(T=>[T,!0])),tabularOnelineClauses:Object.fromEntries(((e=E.tabularOnelineClauses)!=null?e:E.onelineClauses).map(T=>[T,!0]))}};function BO(E){return E.indentStyle===\"tabularLeft\"||E.indentStyle===\"tabularRight\"?\" \".repeat(10):E.useTabs?\"\t\":\" \".repeat(E.tabWidth)}function ZE(E){return E.indentStyle===\"tabularLeft\"||E.indentStyle===\"tabularRight\"}var vO=class{constructor(E){this.params=E,this.index=0}get({key:E,text:e}){return this.params?E?this.params[E]:this.params[this.index++]:e}getPositionalParameterIndex(){return this.index}setPositionalParameterIndex(E){this.index=E}};function FO(E){return E.map(YO).map(VO).map(WO).map(wO).map($O)}var YO=(E,e,T)=>{if(wR(E.type)){const t=xO(T,e);if(t&&t.type===\"PROPERTY_ACCESS_OPERATOR\")return AE(EE({},E),{type:\"IDENTIFIER\",text:E.raw})}return E},VO=(E,e,T)=>{if(E.type===\"RESERVED_FUNCTION_NAME\"){const t=It(T,e);if(!t||!JR(t))return AE(EE({},E),{type:\"RESERVED_KEYWORD\"})}return E},WO=(E,e,T)=>{if(E.type===\"RESERVED_DATA_TYPE\"){const t=It(T,e);if(t&&JR(t))return AE(EE({},E),{type:\"RESERVED_PARAMETERIZED_DATA_TYPE\"})}return E},wO=(E,e,T)=>{if(E.type===\"IDENTIFIER\"){const t=It(T,e);if(t&&qR(t))return AE(EE({},E),{type:\"ARRAY_IDENTIFIER\"})}return E},$O=(E,e,T)=>{if(E.type===\"RESERVED_DATA_TYPE\"){const t=It(T,e);if(t&&qR(t))return AE(EE({},E),{type:\"ARRAY_KEYWORD\"})}return E},xO=(E,e)=>It(E,e,-1),It=(E,e,T=1)=>{let t=1;for(;E[e+t*T]&&XO(E[e+t*T]);)t++;return E[e+t*T]},JR=E=>E.type===\"OPEN_PAREN\"&&E.text===\"(\",qR=E=>E.type===\"OPEN_PAREN\"&&E.text===\"[\",XO=E=>E.type===\"BLOCK_COMMENT\"||E.type===\"LINE_COMMENT\",QR=class{constructor(E){this.tokenize=E,this.index=0,this.tokens=[],this.input=\"\"}reset(E,e){this.input=E,this.index=0,this.tokens=this.tokenize(E)}next(){return this.tokens[this.index++]}save(){}formatError(E){const{line:e,col:T}=KR(this.input,E.start);return`Parse error at token: ${E.text} at line ${e} column ${T}`}has(E){return E in VR}};function ST(E){return E[0]}var Re=new QR(E=>[]),yE=([[E]])=>E,qe=E=>({type:\"keyword\",tokenType:E.type,text:E.text,raw:E.raw}),Ur=E=>({type:\"data_type\",text:E.text,raw:E.raw}),Qe=(E,{leading:e,trailing:T})=>(e!=null&&e.length&&(E=AE(EE({},E),{leadingComments:e})),T!=null&&T.length&&(E=AE(EE({},E),{trailingComments:T})),E),kO=(E,{leading:e,trailing:T})=>{if(e!=null&&e.length){const[t,...r]=E;E=[Qe(t,{leading:e}),...r]}if(T!=null&&T.length){const t=E.slice(0,-1),r=E[E.length-1];E=[...t,Qe(r,{trailing:T})]}return E},KO={Lexer:Re,ParserRules:[{name:\"main$ebnf$1\",symbols:[]},{name:\"main$ebnf$1\",symbols:[\"main$ebnf$1\",\"statement\"],postprocess:E=>E[0].concat([E[1]])},{name:\"main\",symbols:[\"main$ebnf$1\"],postprocess:([E])=>{const e=E[E.length-1];return e&&!e.hasSemicolon?e.children.length>0?E:E.slice(0,-1):E}},{name:\"statement$subexpression$1\",symbols:[Re.has(\"DELIMITER\")?{type:\"DELIMITER\"}:DELIMITER]},{name:\"statement$subexpression$1\",symbols:[Re.has(\"EOF\")?{type:\"EOF\"}:EOF]},{name:\"statement\",symbols:[\"expressions_or_clauses\",\"statement$subexpression$1\"],postprocess:([E,[e]])=>({type:\"statement\",children:E,hasSemicolon:e.type===\"DELIMITER\"})},{name:\"expressions_or_clauses$ebnf$1\",symbols:[]},{name:\"expressions_or_clauses$ebnf$1\",symbols:[\"expressions_or_clauses$ebnf$1\",\"free_form_sql\"],postprocess:E=>E[0].concat([E[1]])},{name:\"expressions_or_clauses$ebnf$2\",symbols:[]},{name:\"expressions_or_clauses$ebnf$2\",symbols:[\"expressions_or_clauses$ebnf$2\",\"clause\"],postprocess:E=>E[0].concat([E[1]])},{name:\"expressions_or_clauses\",symbols:[\"expressions_or_clauses$ebnf$1\",\"expressions_or_clauses$ebnf$2\"],postprocess:([E,e])=>[...E,...e]},{name:\"clause$subexpression$1\",symbols:[\"limit_clause\"]},{name:\"clause$subexpression$1\",symbols:[\"select_clause\"]},{name:\"clause$subexpression$1\",symbols:[\"other_clause\"]},{name:\"clause$subexpression$1\",symbols:[\"set_operation\"]},{name:\"clause\",symbols:[\"clause$subexpression$1\"],postprocess:yE},{name:\"limit_clause$ebnf$1$subexpression$1$ebnf$1\",symbols:[\"free_form_sql\"]},{name:\"limit_clause$ebnf$1$subexpression$1$ebnf$1\",symbols:[\"limit_clause$ebnf$1$subexpression$1$ebnf$1\",\"free_form_sql\"],postprocess:E=>E[0].concat([E[1]])},{name:\"limit_clause$ebnf$1$subexpression$1\",symbols:[Re.has(\"COMMA\")?{type:\"COMMA\"}:COMMA,\"limit_clause$ebnf$1$subexpression$1$ebnf$1\"]},{name:\"limit_clause$ebnf$1\",symbols:[\"limit_clause$ebnf$1$subexpression$1\"],postprocess:ST},{name:\"limit_clause$ebnf$1\",symbols:[],postprocess:()=>null},{name:\"limit_clause\",symbols:[Re.has(\"LIMIT\")?{type:\"LIMIT\"}:LIMIT,\"_\",\"expression_chain_\",\"limit_clause$ebnf$1\"],postprocess:([E,e,T,t])=>{if(t){const[r,R]=t;return{type:\"limit_clause\",limitKw:Qe(qe(E),{trailing:e}),offset:T,count:R}}else return{type:\"limit_clause\",limitKw:Qe(qe(E),{trailing:e}),count:T}}},{name:\"select_clause$subexpression$1$ebnf$1\",symbols:[]},{name:\"select_clause$subexpression$1$ebnf$1\",symbols:[\"select_clause$subexpression$1$ebnf$1\",\"free_form_sql\"],postprocess:E=>E[0].concat([E[1]])},{name:\"select_clause$subexpression$1\",symbols:[\"all_columns_asterisk\",\"select_clause$subexpression$1$ebnf$1\"]},{name:\"select_clause$subexpression$1$ebnf$2\",symbols:[]},{name:\"select_clause$subexpression$1$ebnf$2\",symbols:[\"select_clause$subexpression$1$ebnf$2\",\"free_form_sql\"],postprocess:E=>E[0].concat([E[1]])},{name:\"select_clause$subexpression$1\",symbols:[\"asteriskless_free_form_sql\",\"select_clause$subexpression$1$ebnf$2\"]},{name:\"select_clause\",symbols:[Re.has(\"RESERVED_SELECT\")?{type:\"RESERVED_SELECT\"}:RESERVED_SELECT,\"select_clause$subexpression$1\"],postprocess:([E,[e,T]])=>({type:\"clause\",nameKw:qe(E),children:[e,...T]})},{name:\"select_clause\",symbols:[Re.has(\"RESERVED_SELECT\")?{type:\"RESERVED_SELECT\"}:RESERVED_SELECT],postprocess:([E])=>({type:\"clause\",nameKw:qe(E),children:[]})},{name:\"all_columns_asterisk\",symbols:[Re.has(\"ASTERISK\")?{type:\"ASTERISK\"}:ASTERISK],postprocess:()=>({type:\"all_columns_asterisk\"})},{name:\"other_clause$ebnf$1\",symbols:[]},{name:\"other_clause$ebnf$1\",symbols:[\"other_clause$ebnf$1\",\"free_form_sql\"],postprocess:E=>E[0].concat([E[1]])},{name:\"other_clause\",symbols:[Re.has(\"RESERVED_CLAUSE\")?{type:\"RESERVED_CLAUSE\"}:RESERVED_CLAUSE,\"other_clause$ebnf$1\"],postprocess:([E,e])=>({type:\"clause\",nameKw:qe(E),children:e})},{name:\"set_operation$ebnf$1\",symbols:[]},{name:\"set_operation$ebnf$1\",symbols:[\"set_operation$ebnf$1\",\"free_form_sql\"],postprocess:E=>E[0].concat([E[1]])},{name:\"set_operation\",symbols:[Re.has(\"RESERVED_SET_OPERATION\")?{type:\"RESERVED_SET_OPERATION\"}:RESERVED_SET_OPERATION,\"set_operation$ebnf$1\"],postprocess:([E,e])=>({type:\"set_operation\",nameKw:qe(E),children:e})},{name:\"expression_chain_$ebnf$1\",symbols:[\"expression_with_comments_\"]},{name:\"expression_chain_$ebnf$1\",symbols:[\"expression_chain_$ebnf$1\",\"expression_with_comments_\"],postprocess:E=>E[0].concat([E[1]])},{name:\"expression_chain_\",symbols:[\"expression_chain_$ebnf$1\"],postprocess:ST},{name:\"expression_chain$ebnf$1\",symbols:[]},{name:\"expression_chain$ebnf$1\",symbols:[\"expression_chain$ebnf$1\",\"_expression_with_comments\"],postprocess:E=>E[0].concat([E[1]])},{name:\"expression_chain\",symbols:[\"expression\",\"expression_chain$ebnf$1\"],postprocess:([E,e])=>[E,...e]},{name:\"andless_expression_chain$ebnf$1\",symbols:[]},{name:\"andless_expression_chain$ebnf$1\",symbols:[\"andless_expression_chain$ebnf$1\",\"_andless_expression_with_comments\"],postprocess:E=>E[0].concat([E[1]])},{name:\"andless_expression_chain\",symbols:[\"andless_expression\",\"andless_expression_chain$ebnf$1\"],postprocess:([E,e])=>[E,...e]},{name:\"expression_with_comments_\",symbols:[\"expression\",\"_\"],postprocess:([E,e])=>Qe(E,{trailing:e})},{name:\"_expression_with_comments\",symbols:[\"_\",\"expression\"],postprocess:([E,e])=>Qe(e,{leading:E})},{name:\"_andless_expression_with_comments\",symbols:[\"_\",\"andless_expression\"],postprocess:([E,e])=>Qe(e,{leading:E})},{name:\"free_form_sql$subexpression$1\",symbols:[\"asteriskless_free_form_sql\"]},{name:\"free_form_sql$subexpression$1\",symbols:[\"asterisk\"]},{name:\"free_form_sql\",symbols:[\"free_form_sql$subexpression$1\"],postprocess:yE},{name:\"asteriskless_free_form_sql$subexpression$1\",symbols:[\"asteriskless_andless_expression\"]},{name:\"asteriskless_free_form_sql$subexpression$1\",symbols:[\"logic_operator\"]},{name:\"asteriskless_free_form_sql$subexpression$1\",symbols:[\"comma\"]},{name:\"asteriskless_free_form_sql$subexpression$1\",symbols:[\"comment\"]},{name:\"asteriskless_free_form_sql$subexpression$1\",symbols:[\"other_keyword\"]},{name:\"asteriskless_free_form_sql\",symbols:[\"asteriskless_free_form_sql$subexpression$1\"],postprocess:yE},{name:\"expression$subexpression$1\",symbols:[\"andless_expression\"]},{name:\"expression$subexpression$1\",symbols:[\"logic_operator\"]},{name:\"expression\",symbols:[\"expression$subexpression$1\"],postprocess:yE},{name:\"andless_expression$subexpression$1\",symbols:[\"asteriskless_andless_expression\"]},{name:\"andless_expression$subexpression$1\",symbols:[\"asterisk\"]},{name:\"andless_expression\",symbols:[\"andless_expression$subexpression$1\"],postprocess:yE},{name:\"asteriskless_andless_expression$subexpression$1\",symbols:[\"atomic_expression\"]},{name:\"asteriskless_andless_expression$subexpression$1\",symbols:[\"between_predicate\"]},{name:\"asteriskless_andless_expression$subexpression$1\",symbols:[\"case_expression\"]},{name:\"asteriskless_andless_expression\",symbols:[\"asteriskless_andless_expression$subexpression$1\"],postprocess:yE},{name:\"atomic_expression$subexpression$1\",symbols:[\"array_subscript\"]},{name:\"atomic_expression$subexpression$1\",symbols:[\"function_call\"]},{name:\"atomic_expression$subexpression$1\",symbols:[\"property_access\"]},{name:\"atomic_expression$subexpression$1\",symbols:[\"parenthesis\"]},{name:\"atomic_expression$subexpression$1\",symbols:[\"curly_braces\"]},{name:\"atomic_expression$subexpression$1\",symbols:[\"square_brackets\"]},{name:\"atomic_expression$subexpression$1\",symbols:[\"operator\"]},{name:\"atomic_expression$subexpression$1\",symbols:[\"identifier\"]},{name:\"atomic_expression$subexpression$1\",symbols:[\"parameter\"]},{name:\"atomic_expression$subexpression$1\",symbols:[\"literal\"]},{name:\"atomic_expression$subexpression$1\",symbols:[\"data_type\"]},{name:\"atomic_expression$subexpression$1\",symbols:[\"keyword\"]},{name:\"atomic_expression\",symbols:[\"atomic_expression$subexpression$1\"],postprocess:yE},{name:\"array_subscript\",symbols:[Re.has(\"ARRAY_IDENTIFIER\")?{type:\"ARRAY_IDENTIFIER\"}:ARRAY_IDENTIFIER,\"_\",\"square_brackets\"],postprocess:([E,e,T])=>({type:\"array_subscript\",array:Qe({type:\"identifier\",quoted:!1,text:E.text},{trailing:e}),parenthesis:T})},{name:\"array_subscript\",symbols:[Re.has(\"ARRAY_KEYWORD\")?{type:\"ARRAY_KEYWORD\"}:ARRAY_KEYWORD,\"_\",\"square_brackets\"],postprocess:([E,e,T])=>({type:\"array_subscript\",array:Qe(qe(E),{trailing:e}),parenthesis:T})},{name:\"function_call\",symbols:[Re.has(\"RESERVED_FUNCTION_NAME\")?{type:\"RESERVED_FUNCTION_NAME\"}:RESERVED_FUNCTION_NAME,\"_\",\"parenthesis\"],postprocess:([E,e,T])=>({type:\"function_call\",nameKw:Qe(qe(E),{trailing:e}),parenthesis:T})},{name:\"parenthesis\",symbols:[{literal:\"(\"},\"expressions_or_clauses\",{literal:\")\"}],postprocess:([E,e,T])=>({type:\"parenthesis\",children:e,openParen:\"(\",closeParen:\")\"})},{name:\"curly_braces$ebnf$1\",symbols:[]},{name:\"curly_braces$ebnf$1\",symbols:[\"curly_braces$ebnf$1\",\"free_form_sql\"],postprocess:E=>E[0].concat([E[1]])},{name:\"curly_braces\",symbols:[{literal:\"{\"},\"curly_braces$ebnf$1\",{literal:\"}\"}],postprocess:([E,e,T])=>({type:\"parenthesis\",children:e,openParen:\"{\",closeParen:\"}\"})},{name:\"square_brackets$ebnf$1\",symbols:[]},{name:\"square_brackets$ebnf$1\",symbols:[\"square_brackets$ebnf$1\",\"free_form_sql\"],postprocess:E=>E[0].concat([E[1]])},{name:\"square_brackets\",symbols:[{literal:\"[\"},\"square_brackets$ebnf$1\",{literal:\"]\"}],postprocess:([E,e,T])=>({type:\"parenthesis\",children:e,openParen:\"[\",closeParen:\"]\"})},{name:\"property_access$subexpression$1\",symbols:[\"identifier\"]},{name:\"property_access$subexpression$1\",symbols:[\"array_subscript\"]},{name:\"property_access$subexpression$1\",symbols:[\"all_columns_asterisk\"]},{name:\"property_access$subexpression$1\",symbols:[\"parameter\"]},{name:\"property_access\",symbols:[\"atomic_expression\",\"_\",Re.has(\"PROPERTY_ACCESS_OPERATOR\")?{type:\"PROPERTY_ACCESS_OPERATOR\"}:PROPERTY_ACCESS_OPERATOR,\"_\",\"property_access$subexpression$1\"],postprocess:([E,e,T,t,[r]])=>({type:\"property_access\",object:Qe(E,{trailing:e}),operator:T.text,property:Qe(r,{leading:t})})},{name:\"between_predicate\",symbols:[Re.has(\"BETWEEN\")?{type:\"BETWEEN\"}:BETWEEN,\"_\",\"andless_expression_chain\",\"_\",Re.has(\"AND\")?{type:\"AND\"}:AND,\"_\",\"andless_expression\"],postprocess:([E,e,T,t,r,R,n])=>({type:\"between_predicate\",betweenKw:qe(E),expr1:kO(T,{leading:e,trailing:t}),andKw:qe(r),expr2:[Qe(n,{leading:R})]})},{name:\"case_expression$ebnf$1\",symbols:[\"expression_chain_\"],postprocess:ST},{name:\"case_expression$ebnf$1\",symbols:[],postprocess:()=>null},{name:\"case_expression$ebnf$2\",symbols:[]},{name:\"case_expression$ebnf$2\",symbols:[\"case_expression$ebnf$2\",\"case_clause\"],postprocess:E=>E[0].concat([E[1]])},{name:\"case_expression\",symbols:[Re.has(\"CASE\")?{type:\"CASE\"}:CASE,\"_\",\"case_expression$ebnf$1\",\"case_expression$ebnf$2\",Re.has(\"END\")?{type:\"END\"}:END],postprocess:([E,e,T,t,r])=>({type:\"case_expression\",caseKw:Qe(qe(E),{trailing:e}),endKw:qe(r),expr:T||[],clauses:t})},{name:\"case_clause\",symbols:[Re.has(\"WHEN\")?{type:\"WHEN\"}:WHEN,\"_\",\"expression_chain_\",Re.has(\"THEN\")?{type:\"THEN\"}:THEN,\"_\",\"expression_chain_\"],postprocess:([E,e,T,t,r,R])=>({type:\"case_when\",whenKw:Qe(qe(E),{trailing:e}),thenKw:Qe(qe(t),{trailing:r}),condition:T,result:R})},{name:\"case_clause\",symbols:[Re.has(\"ELSE\")?{type:\"ELSE\"}:ELSE,\"_\",\"expression_chain_\"],postprocess:([E,e,T])=>({type:\"case_else\",elseKw:Qe(qe(E),{trailing:e}),result:T})},{name:\"comma$subexpression$1\",symbols:[Re.has(\"COMMA\")?{type:\"COMMA\"}:COMMA]},{name:\"comma\",symbols:[\"comma$subexpression$1\"],postprocess:([[E]])=>({type:\"comma\"})},{name:\"asterisk$subexpression$1\",symbols:[Re.has(\"ASTERISK\")?{type:\"ASTERISK\"}:ASTERISK]},{name:\"asterisk\",symbols:[\"asterisk$subexpression$1\"],postprocess:([[E]])=>({type:\"operator\",text:E.text})},{name:\"operator$subexpression$1\",symbols:[Re.has(\"OPERATOR\")?{type:\"OPERATOR\"}:OPERATOR]},{name:\"operator\",symbols:[\"operator$subexpression$1\"],postprocess:([[E]])=>({type:\"operator\",text:E.text})},{name:\"identifier$subexpression$1\",symbols:[Re.has(\"IDENTIFIER\")?{type:\"IDENTIFIER\"}:IDENTIFIER]},{name:\"identifier$subexpression$1\",symbols:[Re.has(\"QUOTED_IDENTIFIER\")?{type:\"QUOTED_IDENTIFIER\"}:QUOTED_IDENTIFIER]},{name:\"identifier$subexpression$1\",symbols:[Re.has(\"VARIABLE\")?{type:\"VARIABLE\"}:VARIABLE]},{name:\"identifier\",symbols:[\"identifier$subexpression$1\"],postprocess:([[E]])=>({type:\"identifier\",quoted:E.type!==\"IDENTIFIER\",text:E.text})},{name:\"parameter$subexpression$1\",symbols:[Re.has(\"NAMED_PARAMETER\")?{type:\"NAMED_PARAMETER\"}:NAMED_PARAMETER]},{name:\"parameter$subexpression$1\",symbols:[Re.has(\"QUOTED_PARAMETER\")?{type:\"QUOTED_PARAMETER\"}:QUOTED_PARAMETER]},{name:\"parameter$subexpression$1\",symbols:[Re.has(\"NUMBERED_PARAMETER\")?{type:\"NUMBERED_PARAMETER\"}:NUMBERED_PARAMETER]},{name:\"parameter$subexpression$1\",symbols:[Re.has(\"POSITIONAL_PARAMETER\")?{type:\"POSITIONAL_PARAMETER\"}:POSITIONAL_PARAMETER]},{name:\"parameter$subexpression$1\",symbols:[Re.has(\"CUSTOM_PARAMETER\")?{type:\"CUSTOM_PARAMETER\"}:CUSTOM_PARAMETER]},{name:\"parameter\",symbols:[\"parameter$subexpression$1\"],postprocess:([[E]])=>({type:\"parameter\",key:E.key,text:E.text})},{name:\"literal$subexpression$1\",symbols:[Re.has(\"NUMBER\")?{type:\"NUMBER\"}:NUMBER]},{name:\"literal$subexpression$1\",symbols:[Re.has(\"STRING\")?{type:\"STRING\"}:STRING]},{name:\"literal\",symbols:[\"literal$subexpression$1\"],postprocess:([[E]])=>({type:\"literal\",text:E.text})},{name:\"keyword$subexpression$1\",symbols:[Re.has(\"RESERVED_KEYWORD\")?{type:\"RESERVED_KEYWORD\"}:RESERVED_KEYWORD]},{name:\"keyword$subexpression$1\",symbols:[Re.has(\"RESERVED_PHRASE\")?{type:\"RESERVED_PHRASE\"}:RESERVED_PHRASE]},{name:\"keyword$subexpression$1\",symbols:[Re.has(\"RESERVED_JOIN\")?{type:\"RESERVED_JOIN\"}:RESERVED_JOIN]},{name:\"keyword\",symbols:[\"keyword$subexpression$1\"],postprocess:([[E]])=>qe(E)},{name:\"data_type$subexpression$1\",symbols:[Re.has(\"RESERVED_DATA_TYPE\")?{type:\"RESERVED_DATA_TYPE\"}:RESERVED_DATA_TYPE]},{name:\"data_type\",symbols:[\"data_type$subexpression$1\"],postprocess:([[E]])=>Ur(E)},{name:\"data_type\",symbols:[Re.has(\"RESERVED_PARAMETERIZED_DATA_TYPE\")?{type:\"RESERVED_PARAMETERIZED_DATA_TYPE\"}:RESERVED_PARAMETERIZED_DATA_TYPE,\"_\",\"parenthesis\"],postprocess:([E,e,T])=>({type:\"parameterized_data_type\",dataType:Qe(Ur(E),{trailing:e}),parenthesis:T})},{name:\"logic_operator$subexpression$1\",symbols:[Re.has(\"AND\")?{type:\"AND\"}:AND]},{name:\"logic_operator$subexpression$1\",symbols:[Re.has(\"OR\")?{type:\"OR\"}:OR]},{name:\"logic_operator$subexpression$1\",symbols:[Re.has(\"XOR\")?{type:\"XOR\"}:XOR]},{name:\"logic_operator\",symbols:[\"logic_operator$subexpression$1\"],postprocess:([[E]])=>qe(E)},{name:\"other_keyword$subexpression$1\",symbols:[Re.has(\"WHEN\")?{type:\"WHEN\"}:WHEN]},{name:\"other_keyword$subexpression$1\",symbols:[Re.has(\"THEN\")?{type:\"THEN\"}:THEN]},{name:\"other_keyword$subexpression$1\",symbols:[Re.has(\"ELSE\")?{type:\"ELSE\"}:ELSE]},{name:\"other_keyword$subexpression$1\",symbols:[Re.has(\"END\")?{type:\"END\"}:END]},{name:\"other_keyword\",symbols:[\"other_keyword$subexpression$1\"],postprocess:([[E]])=>qe(E)},{name:\"_$ebnf$1\",symbols:[]},{name:\"_$ebnf$1\",symbols:[\"_$ebnf$1\",\"comment\"],postprocess:E=>E[0].concat([E[1]])},{name:\"_\",symbols:[\"_$ebnf$1\"],postprocess:([E])=>E},{name:\"comment\",symbols:[Re.has(\"LINE_COMMENT\")?{type:\"LINE_COMMENT\"}:LINE_COMMENT],postprocess:([E])=>({type:\"line_comment\",text:E.text,precedingWhitespace:E.precedingWhitespace})},{name:\"comment\",symbols:[Re.has(\"BLOCK_COMMENT\")?{type:\"BLOCK_COMMENT\"}:BLOCK_COMMENT],postprocess:([E])=>({type:\"block_comment\",text:E.text,precedingWhitespace:E.precedingWhitespace})},{name:\"comment\",symbols:[Re.has(\"DISABLE_COMMENT\")?{type:\"DISABLE_COMMENT\"}:DISABLE_COMMENT],postprocess:([E])=>({type:\"disable_comment\",text:E.text,precedingWhitespace:E.precedingWhitespace})}],ParserStart:\"main\"},JO=KO,{Parser:qO,Grammar:QO}=zA;function ZO(E){let e={};const T=new QR(r=>[...FO(E.tokenize(r,e)),WR(r.length)]),t=new qO(QO.fromCompiled(JO),{lexer:T});return{parse:(r,R)=>{e=R;const{results:n}=t.feed(r);if(n.length===1)return n[0];throw n.length===0?new Error(\"Parse error: Invalid SQL\"):new Error(`Parse error: Ambiguous grammar\n${JSON.stringify(n,void 0,2)}`)}}}var ZR=class{constructor(E){this.indentation=E,this.items=[]}add(...E){for(const e of E)switch(e){case 0:this.items.push(0);break;case 1:this.trimHorizontalWhitespace();break;case 2:this.trimWhitespace();break;case 3:this.trimHorizontalWhitespace(),this.addNewline(3);break;case 4:this.trimHorizontalWhitespace(),this.addNewline(4);break;case 5:this.addIndentation();break;case 6:this.items.push(6);break;default:this.items.push(e)}}trimHorizontalWhitespace(){for(;jO(ot(this.items));)this.items.pop()}trimWhitespace(){for(;zO(ot(this.items));)this.items.pop()}addNewline(E){if(this.items.length>0)switch(ot(this.items)){case 3:this.items.pop(),this.items.push(E);break;case 4:break;default:this.items.push(E);break}}addIndentation(){for(let E=0;E<this.indentation.getLevel();E++)this.items.push(6)}toString(){return this.items.map(E=>this.itemToString(E)).join(\"\")}getLayoutItems(){return this.items}itemToString(E){switch(E){case 0:return\" \";case 3:case 4:return`\n`;case 6:return this.indentation.getSingleIndent();default:return E}}},jO=E=>E===0||E===6,zO=E=>E===0||E===6||E===3;function mr(E,e){if(e===\"standard\")return E;let T=[];return E.length>=10&&E.includes(\" \")&&([E,...T]=E.split(\" \")),e===\"tabularLeft\"?E=E.padEnd(9,\" \"):E=E.padStart(9,\" \"),E+[\"\",...T].join(\" \")}function hr(E){return is(E)||E===\"RESERVED_CLAUSE\"||E===\"RESERVED_SELECT\"||E===\"RESERVED_SET_OPERATION\"||E===\"RESERVED_JOIN\"||E===\"LIMIT\"}var oT=\"top-level\",ei=\"block-level\",jR=class{constructor(E){this.indent=E,this.indentTypes=[]}getSingleIndent(){return this.indent}getLevel(){return this.indentTypes.length}increaseTopLevel(){this.indentTypes.push(oT)}increaseBlockLevel(){this.indentTypes.push(ei)}decreaseTopLevel(){this.indentTypes.length>0&&ot(this.indentTypes)===oT&&this.indentTypes.pop()}decreaseBlockLevel(){for(;this.indentTypes.length>0&&this.indentTypes.pop()===oT;);}},Ei=class extends ZR{constructor(E){super(new jR(\"\")),this.expressionWidth=E,this.length=0,this.trailingSpace=!1}add(...E){if(E.forEach(e=>this.addToLength(e)),this.length>this.expressionWidth)throw new NT;super.add(...E)}addToLength(E){if(typeof E==\"string\")this.length+=E.length,this.trailingSpace=!1;else{if(E===4||E===3)throw new NT;E===5||E===6||E===0?this.trailingSpace||(this.length++,this.trailingSpace=!0):(E===2||E===1)&&this.trailingSpace&&(this.trailingSpace=!1,this.length--)}}},NT=class extends Error{},ti=class lT{constructor({cfg:e,dialectCfg:T,params:t,layout:r,inline:R=!1}){this.inline=!1,this.nodes=[],this.index=-1,this.cfg=e,this.dialectCfg=T,this.inline=R,this.params=t,this.layout=r}format(e){for(this.nodes=e,this.index=0;this.index<this.nodes.length;this.index++)this.formatNode(this.nodes[this.index]);return this.layout}formatNode(e){this.formatComments(e.leadingComments),this.formatNodeWithoutComments(e),this.formatComments(e.trailingComments)}formatNodeWithoutComments(e){switch(e.type){case\"function_call\":return this.formatFunctionCall(e);case\"parameterized_data_type\":return this.formatParameterizedDataType(e);case\"array_subscript\":return this.formatArraySubscript(e);case\"property_access\":return this.formatPropertyAccess(e);case\"parenthesis\":return this.formatParenthesis(e);case\"between_predicate\":return this.formatBetweenPredicate(e);case\"case_expression\":return this.formatCaseExpression(e);case\"case_when\":return this.formatCaseWhen(e);case\"case_else\":return this.formatCaseElse(e);case\"clause\":return this.formatClause(e);case\"set_operation\":return this.formatSetOperation(e);case\"limit_clause\":return this.formatLimitClause(e);case\"all_columns_asterisk\":return this.formatAllColumnsAsterisk(e);case\"literal\":return this.formatLiteral(e);case\"identifier\":return this.formatIdentifier(e);case\"parameter\":return this.formatParameter(e);case\"operator\":return this.formatOperator(e);case\"comma\":return this.formatComma(e);case\"line_comment\":return this.formatLineComment(e);case\"block_comment\":return this.formatBlockComment(e);case\"disable_comment\":return this.formatBlockComment(e);case\"data_type\":return this.formatDataType(e);case\"keyword\":return this.formatKeywordNode(e)}}formatFunctionCall(e){this.withComments(e.nameKw,()=>{this.layout.add(this.showFunctionKw(e.nameKw))}),this.formatNode(e.parenthesis)}formatParameterizedDataType(e){this.withComments(e.dataType,()=>{this.layout.add(this.showDataType(e.dataType))}),this.formatNode(e.parenthesis)}formatArraySubscript(e){let T;switch(e.array.type){case\"data_type\":T=this.showDataType(e.array);break;case\"keyword\":T=this.showKw(e.array);break;default:T=this.showIdentifier(e.array);break}this.withComments(e.array,()=>{this.layout.add(T)}),this.formatNode(e.parenthesis)}formatPropertyAccess(e){this.formatNode(e.object),this.layout.add(1,e.operator),this.formatNode(e.property)}formatParenthesis(e){const T=this.formatInlineExpression(e.children);T?(this.layout.add(e.openParen),this.layout.add(...T.getLayoutItems()),this.layout.add(1,e.closeParen,0)):(this.layout.add(e.openParen,3),ZE(this.cfg)?(this.layout.add(5),this.layout=this.formatSubExpression(e.children)):(this.layout.indentation.increaseBlockLevel(),this.layout.add(5),this.layout=this.formatSubExpression(e.children),this.layout.indentation.decreaseBlockLevel()),this.layout.add(3,5,e.closeParen,0))}formatBetweenPredicate(e){this.layout.add(this.showKw(e.betweenKw),0),this.layout=this.formatSubExpression(e.expr1),this.layout.add(1,0,this.showNonTabularKw(e.andKw),0),this.layout=this.formatSubExpression(e.expr2),this.layout.add(0)}formatCaseExpression(e){this.formatNode(e.caseKw),this.layout.indentation.increaseBlockLevel(),this.layout=this.formatSubExpression(e.expr),this.layout=this.formatSubExpression(e.clauses),this.layout.indentation.decreaseBlockLevel(),this.layout.add(3,5),this.formatNode(e.endKw)}formatCaseWhen(e){this.layout.add(3,5),this.formatNode(e.whenKw),this.layout=this.formatSubExpression(e.condition),this.formatNode(e.thenKw),this.layout=this.formatSubExpression(e.result)}formatCaseElse(e){this.layout.add(3,5),this.formatNode(e.elseKw),this.layout=this.formatSubExpression(e.result)}formatClause(e){this.isOnelineClause(e)?this.formatClauseInOnelineStyle(e):ZE(this.cfg)?this.formatClauseInTabularStyle(e):this.formatClauseInIndentedStyle(e)}isOnelineClause(e){return ZE(this.cfg)?this.dialectCfg.tabularOnelineClauses[e.nameKw.text]:this.dialectCfg.onelineClauses[e.nameKw.text]}formatClauseInIndentedStyle(e){this.layout.add(3,5,this.showKw(e.nameKw),3),this.layout.indentation.increaseTopLevel(),this.layout.add(5),this.layout=this.formatSubExpression(e.children),this.layout.indentation.decreaseTopLevel()}formatClauseInOnelineStyle(e){this.layout.add(3,5,this.showKw(e.nameKw),0),this.layout=this.formatSubExpression(e.children)}formatClauseInTabularStyle(e){this.layout.add(3,5,this.showKw(e.nameKw),0),this.layout.indentation.increaseTopLevel(),this.layout=this.formatSubExpression(e.children),this.layout.indentation.decreaseTopLevel()}formatSetOperation(e){this.layout.add(3,5,this.showKw(e.nameKw),3),this.layout.add(5),this.layout=this.formatSubExpression(e.children)}formatLimitClause(e){this.withComments(e.limitKw,()=>{this.layout.add(3,5,this.showKw(e.limitKw))}),this.layout.indentation.increaseTopLevel(),ZE(this.cfg)?this.layout.add(0):this.layout.add(3,5),e.offset?(this.layout=this.formatSubExpression(e.offset),this.layout.add(1,\",\",0),this.layout=this.formatSubExpression(e.count)):this.layout=this.formatSubExpression(e.count),this.layout.indentation.decreaseTopLevel()}formatAllColumnsAsterisk(e){this.layout.add(\"*\",0)}formatLiteral(e){this.layout.add(e.text,0)}formatIdentifier(e){this.layout.add(this.showIdentifier(e),0)}formatParameter(e){this.layout.add(this.params.get(e),0)}formatOperator({text:e}){this.cfg.denseOperators||this.dialectCfg.alwaysDenseOperators.includes(e)?this.layout.add(1,e):e===\":\"?this.layout.add(1,e,0):this.layout.add(e,0)}formatComma(e){this.inline?this.layout.add(1,\",\",0):this.layout.add(1,\",\",3,5)}withComments(e,T){this.formatComments(e.leadingComments),T(),this.formatComments(e.trailingComments)}formatComments(e){e&&e.forEach(T=>{T.type===\"line_comment\"?this.formatLineComment(T):this.formatBlockComment(T)})}formatLineComment(e){AT(e.precedingWhitespace||\"\")?this.layout.add(3,5,e.text,4,5):this.layout.getLayoutItems().length>0?this.layout.add(2,0,e.text,4,5):this.layout.add(e.text,4,5)}formatBlockComment(e){e.type===\"block_comment\"&&this.isMultilineBlockComment(e)?(this.splitBlockComment(e.text).forEach(T=>{this.layout.add(3,5,T)}),this.layout.add(3,5)):this.layout.add(e.text,0)}isMultilineBlockComment(e){return AT(e.text)||AT(e.precedingWhitespace||\"\")}isDocComment(e){const T=e.split(/\\\\n/);return/^\\\\/\\\\*\\\\*?$/.test(T[0])&&T.slice(1,T.length-1).every(t=>/^\\\\s*\\\\*/.test(t))&&/^\\\\s*\\\\*\\\\/$/.test(ot(T))}splitBlockComment(e){return this.isDocComment(e)?e.split(/\\\\n/).map(T=>/^\\\\s*\\\\*/.test(T)?\" \"+T.replace(/^\\\\s*/,\"\"):T):e.split(/\\\\n/).map(T=>T.replace(/^\\\\s*/,\"\"))}formatSubExpression(e){return new lT({cfg:this.cfg,dialectCfg:this.dialectCfg,params:this.params,layout:this.layout,inline:this.inline}).format(e)}formatInlineExpression(e){const T=this.params.getPositionalParameterIndex();try{return new lT({cfg:this.cfg,dialectCfg:this.dialectCfg,params:this.params,layout:new Ei(this.cfg.expressionWidth),inline:!0}).format(e)}catch(t){if(t instanceof NT){this.params.setPositionalParameterIndex(T);return}else throw t}}formatKeywordNode(e){switch(e.tokenType){case\"RESERVED_JOIN\":return this.formatJoin(e);case\"AND\":case\"OR\":case\"XOR\":return this.formatLogicalOperator(e);default:return this.formatKeyword(e)}}formatJoin(e){ZE(this.cfg)?(this.layout.indentation.decreaseTopLevel(),this.layout.add(3,5,this.showKw(e),0),this.layout.indentation.increaseTopLevel()):this.layout.add(3,5,this.showKw(e),0)}formatKeyword(e){this.layout.add(this.showKw(e),0)}formatLogicalOperator(e){this.cfg.logicalOperatorNewline===\"before\"?ZE(this.cfg)?(this.layout.indentation.decreaseTopLevel(),this.layout.add(3,5,this.showKw(e),0),this.layout.indentation.increaseTopLevel()):this.layout.add(3,5,this.showKw(e),0):this.layout.add(this.showKw(e),3,5)}formatDataType(e){this.layout.add(this.showDataType(e),0)}showKw(e){return hr(e.tokenType)?mr(this.showNonTabularKw(e),this.cfg.indentStyle):this.showNonTabularKw(e)}showNonTabularKw(e){switch(this.cfg.keywordCase){case\"preserve\":return Dt(e.raw);case\"upper\":return e.text;case\"lower\":return e.text.toLowerCase()}}showFunctionKw(e){return hr(e.tokenType)?mr(this.showNonTabularFunctionKw(e),this.cfg.indentStyle):this.showNonTabularFunctionKw(e)}showNonTabularFunctionKw(e){switch(this.cfg.functionCase){case\"preserve\":return Dt(e.raw);case\"upper\":return e.text;case\"lower\":return e.text.toLowerCase()}}showIdentifier(e){if(e.quoted)return e.text;switch(this.cfg.identifierCase){case\"preserve\":return e.text;case\"upper\":return e.text.toUpperCase();case\"lower\":return e.text.toLowerCase()}}showDataType(e){switch(this.cfg.dataTypeCase){case\"preserve\":return Dt(e.raw);case\"upper\":return e.text;case\"lower\":return e.text.toLowerCase()}}},Ti=class{constructor(E,e){this.dialect=E,this.cfg=e,this.params=new vO(this.cfg.params)}format(E){const e=this.parse(E);return this.formatAst(e).trimEnd()}parse(E){return ZO(this.dialect.tokenizer).parse(E,this.cfg.paramTypes||{})}formatAst(E){return E.map(e=>this.formatStatement(e)).join(`\n`.repeat(this.cfg.linesBetweenQueries+1))}formatStatement(E){const e=new ti({cfg:this.cfg,dialectCfg:this.dialect.formatOptions,params:this.params,layout:new ZR(new jR(BO(this.cfg)))}).format(E.children);return E.hasSemicolon&&(this.cfg.newlineBeforeSemicolon?e.add(3,\";\"):e.add(2,\";\")),e.toString()}},_T=class extends Error{};function ri(E){const e=[\"multilineLists\",\"newlineBeforeOpenParen\",\"newlineBeforeCloseParen\",\"aliasAs\",\"commaPosition\",\"tabulateAlias\"];for(const T of e)if(T in E)throw new _T(`${T} config is no more supported.`);if(E.expressionWidth<=0)throw new _T(`expressionWidth config must be positive number. Received ${E.expressionWidth} instead.`);return E.params&&!Ri(E.params)&&console.warn('WARNING: All \"params\" option values should be strings.'),E}function Ri(E){return(E instanceof Array?E:Object.values(E)).every(T=>typeof T==\"string\")}var zR={bigquery:\"bigquery\",db2:\"db2\",db2i:\"db2i\",hive:\"hive\",mariadb:\"mariadb\",mysql:\"mysql\",n1ql:\"n1ql\",plsql:\"plsql\",postgresql:\"postgresql\",redshift:\"redshift\",spark:\"spark\",sqlite:\"sqlite\",sql:\"sql\",tidb:\"tidb\",trino:\"trino\",transactsql:\"transactsql\",tsql:\"transactsql\",singlestoredb:\"singlestoredb\",snowflake:\"snowflake\"},ni=Object.keys(zR),Ai={tabWidth:2,useTabs:!1,keywordCase:\"preserve\",identifierCase:\"preserve\",dataTypeCase:\"preserve\",functionCase:\"preserve\",indentStyle:\"standard\",logicalOperatorNewline:\"before\",expressionWidth:50,linesBetweenQueries:1,denseOperators:!1,newlineBeforeSemicolon:!1},Gr=(E,e={})=>{if(typeof e.language==\"string\"&&!ni.includes(e.language))throw new _T(`Unsupported SQL dialect: ${e.language}`);const T=zR[e.language||\"sql\"];return si(E,AE(EE({},e),{dialect:YR[T]}))},si=(E,e)=>{var T=e,{dialect:t}=T,r=ts(T,[\"dialect\"]);if(typeof E!=\"string\")throw new Error(\"Invalid query argument. Expected string, instead got \"+typeof E);const R=ri(EE(EE({},Ai),r));return new Ti(HO(t),R).format(E)};function gr(E,e,T){const t=E.slice();return t[1]=e[T],t}function Hr(E,e){let T,t=e[1].name+\"\",r,R,n,s=e[1].instantiated_value+\"\",S,A;return{key:E,first:null,c(){T=f(\"b\"),r=te(t),R=te(\":\"),n=$(),S=te(s),A=f(\"br\"),this.first=T},m(o,i){V(o,T,i),l(T,r),l(T,R),V(o,n,i),V(o,S,i),V(o,A,i)},p(o,i){e=o,i&1&&t!==(t=e[1].name+\"\")&&Le(r,t),i&1&&s!==(s=e[1].instantiated_value+\"\")&&Le(S,s)},d(o){o&&(Y(T),Y(n),Y(S),Y(A))}}}function Si(E){let e,T,t,r,R,n,s,S=E[0].function_name+\"\",A,o,i,_=[],c=new Map,P,p,C,L,I,u,H,b=E[0].description+\"\",M,O,N,D,B=Gr(E[0].instantiated_sql)+\"\",h,G,F,W,x=E[0].sql_template+\"\",J,oe=De(E[0].arguments);const z=Oe=>Oe[1].name;for(let Oe=0;Oe<oe.length;Oe+=1){let Ue=gr(E,oe,Oe),ye=z(Ue);c.set(ye,_[Oe]=Hr(ye,Ue))}return{c(){e=f(\"div\"),T=f(\"div\"),t=f(\"div\"),t.innerHTML='<span class=\"inline-flex justify-center items-center size-8 rounded-full border-4 border-teal-100 bg-teal-200 text-teal-800 dark:border-teal-900 dark:bg-teal-800 dark:text-teal-400\"><svg class=\"flex-shrink-0 size-4\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z\"></path><path d=\"m9 12 2 2 4-4\"></path></svg></span>',r=$(),R=f(\"div\"),n=f(\"h3\"),s=te(\"Function: \"),A=te(S),o=$(),i=f(\"p\");for(let Oe=0;Oe<_.length;Oe+=1)_[Oe].c();P=$(),p=f(\"div\"),C=f(\"div\"),C.innerHTML='<nav class=\"flex space-x-2\" aria-label=\"Tabs\"><button type=\"button\" class=\"hs-tab-active:font-semibold hs-tab-active:border-blue-600 hs-tab-active:text-blue-600 py-4 px-1 inline-flex items-center gap-x-2 border-b-2 border-transparent text-sm whitespace-nowrap text-gray-500 hover:text-blue-600 disabled:opacity-50 disabled:pointer-events-none dark:text-neutral-400 dark:hover:text-blue-500 active\" id=\"basic-tabs-item-1\" data-hs-tab=\"#basic-tabs-1\" aria-controls=\"basic-tabs-1\" role=\"tab\">Description</button> <button type=\"button\" class=\"hs-tab-active:font-semibold hs-tab-active:border-blue-600 hs-tab-active:text-blue-600 py-4 px-1 inline-flex items-center gap-x-2 border-b-2 border-transparent text-sm whitespace-nowrap text-gray-500 hover:text-blue-600 disabled:opacity-50 disabled:pointer-events-none dark:text-neutral-400 dark:hover:text-blue-500\" id=\"basic-tabs-item-2\" data-hs-tab=\"#basic-tabs-2\" aria-controls=\"basic-tabs-2\" role=\"tab\">SQL</button> <button type=\"button\" class=\"hs-tab-active:font-semibold hs-tab-active:border-blue-600 hs-tab-active:text-blue-600 py-4 px-1 inline-flex items-center gap-x-2 border-b-2 border-transparent text-sm whitespace-nowrap text-gray-500 hover:text-blue-600 disabled:opacity-50 disabled:pointer-events-none dark:text-neutral-400 dark:hover:text-blue-500\" id=\"basic-tabs-item-3\" data-hs-tab=\"#basic-tabs-3\" aria-controls=\"basic-tabs-3\" role=\"tab\">Template</button></nav>',L=$(),I=f(\"div\"),u=f(\"div\"),H=f(\"p\"),M=te(b),O=$(),N=f(\"div\"),D=f(\"p\"),h=te(B),G=$(),F=f(\"div\"),W=f(\"p\"),J=te(x),a(t,\"class\",\"flex-shrink-0\"),a(n,\"class\",\"text-gray-800 font-semibold dark:text-white\"),a(i,\"class\",\"text-sm text-gray-700 dark:text-neutral-400\"),a(R,\"class\",\"ms-3\"),a(T,\"class\",\"flex\"),a(C,\"class\",\"border-b border-gray-200 px-4 dark:border-neutral-700\"),a(H,\"class\",\"text-gray-500 dark:text-neutral-400\"),a(u,\"id\",\"basic-tabs-1\"),a(u,\"role\",\"tabpanel\"),a(u,\"aria-labelledby\",\"basic-tabs-item-1\"),a(D,\"class\",\"text-gray-500 dark:text-neutral-400 font-mono text-teal-800\"),a(N,\"id\",\"basic-tabs-2\"),a(N,\"class\",\"hidden\"),a(N,\"role\",\"tabpanel\"),a(N,\"aria-labelledby\",\"basic-tabs-item-2\"),a(W,\"class\",\"text-gray-500 dark:text-neutral-400 font-mono\"),a(F,\"id\",\"basic-tabs-3\"),a(F,\"class\",\"hidden\"),a(F,\"role\",\"tabpanel\"),a(F,\"aria-labelledby\",\"basic-tabs-item-3\"),a(I,\"class\",\"px-4\"),a(p,\"class\",\"w-full bg-white rounded-lg shadow-md dark:bg-neutral-800\"),a(e,\"class\",\"bg-teal-50 border-t-2 border-teal-500 rounded-lg p-4 dark:bg-teal-800/30\"),a(e,\"role\",\"alert\")},m(Oe,Ue){V(Oe,e,Ue),l(e,T),l(T,t),l(T,r),l(T,R),l(R,n),l(n,s),l(n,A),l(R,o),l(R,i);for(let ye=0;ye<_.length;ye+=1)_[ye]&&_[ye].m(i,null);l(e,P),l(e,p),l(p,C),l(p,L),l(p,I),l(I,u),l(u,H),l(H,M),l(I,O),l(I,N),l(N,D),l(D,h),l(I,G),l(I,F),l(F,W),l(W,J)},p(Oe,[Ue]){Ue&1&&S!==(S=Oe[0].function_name+\"\")&&Le(A,S),Ue&1&&(oe=De(Oe[0].arguments),_=un(_,Ue,z,1,Oe,oe,c,i,Cn,Hr,null,gr)),Ue&1&&b!==(b=Oe[0].description+\"\")&&Le(M,b),Ue&1&&B!==(B=Gr(Oe[0].instantiated_sql)+\"\")&&Le(h,B),Ue&1&&x!==(x=Oe[0].sql_template+\"\")&&Le(J,x)},i:j,o:j,d(Oe){Oe&&Y(e);for(let Ue=0;Ue<_.length;Ue+=1)_[Ue].d()}}}function oi(E,e,T){let{functionData:t}=e;return E.$$set=r=>{\"functionData\"in r&&T(0,t=r.functionData)},[t]}class Oi extends ue{constructor(e){super(),Ce(this,e,oi,Si,_e,{functionData:0})}}function br(E,e,T){const t=E.slice();return t[12]=e[T],t}function yr(E,e,T){const t=E.slice();return t[12]=e[T],t[13]=e,t[14]=T,t}function ii(E){let e,T,t,r=E[0].function_name+\"\",R,n,s,S=E[0].description+\"\",A,o,i,_,c,P,p,C,L,I=De(E[0].arguments),u=[];for(let H=0;H<I.length;H+=1)u[H]=Br(br(E,I,H));return{c(){e=f(\"div\"),T=f(\"div\"),t=f(\"h3\"),R=te(r),n=$(),s=f(\"p\"),A=te(S),o=$(),i=f(\"ul\");for(let H=0;H<u.length;H+=1)u[H].c();_=$(),c=f(\"button\"),c.textContent=\"Delete\",P=$(),p=f(\"button\"),p.textContent=\"Edit\",a(t,\"class\",\"text-lg font-bold text-gray-800 dark:text-white\"),a(s,\"class\",\"mt-1 text-xs font-medium uppercase text-gray-500 dark:text-neutral-500\"),a(i,\"class\",\"mt-1 marker:text-blue-600 list-disc ps-5 space-y-2 text-sm text-gray-600 dark:text-neutral-400\"),a(T,\"class\",\"flex-grow\"),a(c,\"class\",\"mt-3 inline-flex items-center gap-x-1 text-sm font-semibold rounded-lg border border-red-600 text-red-600 hover:border-red-500 hover:text-red-500 disabled:opacity-50 disabled:pointer-events-none dark:border-red-500 dark:text-red-500 dark:hover:text-red-400 dark:hover:border-red-400 py-1 px-4 max-w-fit\"),a(p,\"class\",\"mt-3 inline-flex items-center gap-x-1 text-sm font-semibold rounded-lg border border-blue-600 text-blue-600 hover:border-blue-500 hover:text-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:border-blue-500 dark:text-blue-500 dark:hover:text-blue-400 dark:hover:border-blue-400 py-1 px-4 max-w-fit\"),a(e,\"class\",\"p-4 flex flex-col bg-white border border-t-4 border-t-blue-600 shadow-sm rounded-xl dark:bg-neutral-900 dark:border-neutral-7000 dark:border-t-blue-500 dark:shadow-neutral-700/70\")},m(H,b){V(H,e,b),l(e,T),l(T,t),l(t,R),l(T,n),l(T,s),l(s,A),l(T,o),l(T,i);for(let M=0;M<u.length;M+=1)u[M]&&u[M].m(i,null);l(e,_),l(e,c),l(e,P),l(e,p),C||(L=[Ne(c,\"click\",E[10]),Ne(p,\"click\",E[11])],C=!0)},p(H,b){if(b&1&&r!==(r=H[0].function_name+\"\")&&Le(R,r),b&1&&S!==(S=H[0].description+\"\")&&Le(A,S),b&1){I=De(H[0].arguments);let M;for(M=0;M<I.length;M+=1){const O=br(H,I,M);u[M]?u[M].p(O,b):(u[M]=Br(O),u[M].c(),u[M].m(i,null))}for(;M<u.length;M+=1)u[M].d(1);u.length=I.length}},d(H){H&&Y(e),nE(u,H),C=!1,NE(L)}}}function ai(E){let e,T,t,r,R,n,s,S,A,o,i,_,c,P,p,C,L,I,u,H,b,M,O,N,D,B=De(E[1].arguments),h=[];for(let G=0;G<B.length;G+=1)h[G]=vr(yr(E,B,G));return{c(){e=f(\"div\"),T=f(\"form\"),t=f(\"div\"),r=f(\"div\"),R=f(\"span\"),R.textContent=\"Function Name\",n=$(),s=f(\"input\"),S=$(),A=f(\"div\"),o=f(\"span\"),o.textContent=\"Function Description\",i=$(),_=f(\"textarea\"),c=$(),P=f(\"div\"),p=f(\"span\"),p.textContent=\"SQL Template\",C=$(),L=f(\"textarea\"),I=$();for(let G=0;G<h.length;G+=1)h[G].c();u=$(),H=f(\"div\"),b=f(\"button\"),b.textContent=\"Cancel\",M=$(),O=f(\"button\"),O.textContent=\"Save\",a(R,\"class\",\"inline-block text-sm font-bold text-gray-800 mt-2.5 dark:text-neutral-200\"),a(s,\"type\",\"text\"),a(s,\"class\",\"py-2 px-3 pe-11 block w-full border-gray-200 shadow-sm rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:placeholder-neutral-500 dark:focus:ring-neutral-600\"),a(s,\"placeholder\",\"Enter function name\"),a(r,\"class\",\"space-y-2\"),a(t,\"class\",\"space-y-4 sm:space-y-6\"),a(o,\"class\",\"inline-block text-sm font-bold text-gray-800 mt-2.5 dark:text-neutral-200\"),a(_,\"class\",\"py-2 px-3 pe-11 block w-full border-gray-200 shadow-sm rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:placeholder-neutral-500 dark:focus:ring-neutral-600\"),a(_,\"placeholder\",\"Enter function description\"),a(A,\"class\",\"space-y-2\"),a(p,\"class\",\"inline-block text-sm font-bold text-gray-800 mt-2.5 dark:text-neutral-200\"),a(L,\"class\",\"py-2 px-3 pe-11 block w-full border-gray-200 shadow-sm rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:placeholder-neutral-500 dark:focus:ring-neutral-600 font-mono\"),a(L,\"placeholder\",\"Enter function description\"),a(P,\"class\",\"space-y-2\"),a(b,\"class\",\"py-2 px-3 inline-flex items-center gap-x-2 text-sm font-semibold rounded-lg border border-red-500 text-red-500 hover:border-red-400 hover:text-red-400 disabled:opacity-50 disabled:pointer-events-none\"),a(O,\"class\",\"py-2 px-3 inline-flex items-center gap-x-2 text-sm font-semibold rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none\"),a(H,\"class\",\"mt-3 flex gap-x-2\"),a(e,\"class\",\"p-4 flex flex-col bg-white border border-t-4 border-t-red-600 shadow-sm rounded-xl dark:bg-neutral-900 dark:border-neutral-7000 dark:border-t-red-500 dark:shadow-neutral-700/70\")},m(G,F){V(G,e,F),l(e,T),l(T,t),l(t,r),l(r,R),l(r,n),l(r,s),Ye(s,E[1].function_name),l(T,S),l(T,A),l(A,o),l(A,i),l(A,_),Ye(_,E[1].description),l(T,c),l(T,P),l(P,p),l(P,C),l(P,L),Ye(L,E[1].sql_template),l(T,I);for(let W=0;W<h.length;W+=1)h[W]&&h[W].m(T,null);l(e,u),l(e,H),l(H,b),l(H,M),l(H,O),N||(D=[Ne(s,\"input\",E[2]),Ne(_,\"input\",E[3]),Ne(L,\"input\",E[4]),Ne(b,\"click\",E[8]),Ne(O,\"click\",E[9])],N=!0)},p(G,F){if(F&2&&s.value!==G[1].function_name&&Ye(s,G[1].function_name),F&2&&Ye(_,G[1].description),F&2&&Ye(L,G[1].sql_template),F&2){B=De(G[1].arguments);let W;for(W=0;W<B.length;W+=1){const x=yr(G,B,W);h[W]?h[W].p(x,F):(h[W]=vr(x),h[W].c(),h[W].m(T,null))}for(;W<h.length;W+=1)h[W].d(1);h.length=B.length}},d(G){G&&Y(e),nE(h,G),N=!1,NE(D)}}}function Br(E){let e,T,t=E[12].name+\"\",r,R,n,s=E[12].description+\"\",S;return{c(){e=f(\"li\"),T=f(\"b\"),r=te(t),R=te(\":\"),n=$(),S=te(s)},m(A,o){V(A,e,o),l(e,T),l(T,r),l(T,R),l(e,n),l(e,S)},p(A,o){o&1&&t!==(t=A[12].name+\"\")&&Le(r,t),o&1&&s!==(s=A[12].description+\"\")&&Le(S,s)},d(A){A&&Y(e)}}}function vr(E){let e,T,t,r,R,n,s,S,A,o,i,_,c,P,p,C,L,I,u,H,b;function M(){E[5].call(R,E[13],E[14])}function O(){E[6].call(o,E[13],E[14])}function N(){E[7].call(p,E[13],E[14])}return{c(){e=f(\"div\"),T=f(\"div\"),t=f(\"span\"),t.textContent=\"Argument Name\",r=$(),R=f(\"input\"),n=$(),s=f(\"div\"),S=f(\"span\"),S.textContent=\"Argument Description\",A=$(),o=f(\"input\"),i=$(),_=f(\"div\"),c=f(\"span\"),c.textContent=\"Argument Type\",P=$(),p=f(\"select\"),C=f(\"option\"),C.textContent=\"String\",L=f(\"option\"),L.textContent=\"Numeric\",I=f(\"option\"),I.textContent=\"Boolean\",u=$(),a(t,\"class\",\"inline-block text-sm font-bold text-gray-800 mt-2.5 dark:text-neutral-200\"),a(R,\"type\",\"text\"),a(R,\"class\",\"py-2 px-3 pe-11 block w-full border-gray-200 shadow-sm rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:placeholder-neutral-500 dark:focus:ring-neutral-600\"),a(R,\"placeholder\",\"Enter argument name\"),a(T,\"class\",\"space-y-2\"),a(S,\"class\",\"inline-block text-sm font-bold text-gray-800 mt-2.5 dark:text-neutral-200\"),a(o,\"type\",\"text\"),a(o,\"class\",\"py-2 px-3 pe-11 block w-full border-gray-200 shadow-sm rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:placeholder-neutral-500 dark:focus:ring-neutral-600\"),a(o,\"placeholder\",\"Enter argument description\"),a(s,\"class\",\"space-y-2\"),a(c,\"class\",\"inline-block text-sm font-bold text-gray-800 mt-2.5 dark:text-neutral-200\"),C.__value=\"STRING\",Ye(C,C.__value),L.__value=\"NUMERIC\",Ye(L,L.__value),I.__value=\"BOOLEAN\",Ye(I,I.__value),a(p,\"class\",\"py-2 px-3 pe-11 block w-full border-gray-200 shadow-sm rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:placeholder-neutral-500 dark:focus:ring-neutral-600\"),E[12].general_type===void 0&&dt(N),a(_,\"class\",\"space-y-2\"),a(e,\"class\",\"space-y-4\")},m(D,B){V(D,e,B),l(e,T),l(T,t),l(T,r),l(T,R),Ye(R,E[12].name),l(e,n),l(e,s),l(s,S),l(s,A),l(s,o),Ye(o,E[12].description),l(e,i),l(e,_),l(_,c),l(_,P),l(_,p),l(p,C),l(p,L),l(p,I),WT(p,E[12].general_type,!0),l(e,u),H||(b=[Ne(R,\"input\",M),Ne(o,\"input\",O),Ne(p,\"change\",N)],H=!0)},p(D,B){E=D,B&2&&R.value!==E[12].name&&Ye(R,E[12].name),B&2&&o.value!==E[12].description&&Ye(o,E[12].description),B&2&&WT(p,E[12].general_type)},d(D){D&&Y(e),H=!1,NE(b)}}}function Ii(E){let e;function T(R,n){return R[1]!=null?ai:ii}let t=T(E),r=t(E);return{c(){r.c(),e=je()},m(R,n){r.m(R,n),V(R,e,n)},p(R,[n]){t===(t=T(R))&&r?r.p(R,n):(r.d(1),r=t(R),r&&(r.c(),r.m(e.parentNode,e)))},i:j,o:j,d(R){R&&Y(e),r.d(R)}}}function Ni(E,e,T){let{functionTemplate:t}=e,r=null;function R(){r.function_name=this.value,T(1,r)}function n(){r.description=this.value,T(1,r)}function s(){r.sql_template=this.value,T(1,r)}function S(p,C){p[C].name=this.value,T(1,r)}function A(p,C){p[C].description=this.value,T(1,r)}function o(p,C){p[C].general_type=On(this),T(1,r)}const i=()=>{T(1,r=null)},_=()=>{r&&(Vn(t.function_name,r),T(0,t=r),T(1,r=null))},c=()=>{t&&window.confirm(\"Are you sure you want to delete this function?\")&&Wn(t.function_name)},P=()=>{T(1,r=structuredClone(t))};return E.$$set=p=>{\"functionTemplate\"in p&&T(0,t=p.functionTemplate)},[t,r,R,n,s,S,A,o,i,_,c,P]}class en extends ue{constructor(e){super(),Ce(this,e,Ni,Ii,_e,{functionTemplate:0})}}function Fr(E,e,T){const t=E.slice();return t[15]=e[T],t}function Yr(E,e,T){const t=E.slice();return t[18]=e[T],t}function Vr(E,e,T){const t=E.slice();return t[21]=e[T],t}function Wr(E,e,T){const t=E.slice();return t[21]=e[T],t}function wr(E){let e,T;return e=new ZA({}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function $r(E){let e,T;return e=new Ze({props:{$$slots:{default:[_i]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&67108866&&(R.$$scope={dirty:r,ctx:t}),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function xr(E){let e,T;return e=new IE({props:{message:E[21],onSubmit:uT}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&2&&(R.message=t[21]),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function li(E){let e=E[1].header+\"\",T,t,r,R,n=De(E[1].questions),s=[];for(let A=0;A<n.length;A+=1)s[A]=xr(Wr(E,n,A));const S=A=>y(s[A],1,1,()=>{s[A]=null});return{c(){T=te(e),t=$();for(let A=0;A<s.length;A+=1)s[A].c();r=je()},m(A,o){V(A,T,o),V(A,t,o);for(let i=0;i<s.length;i+=1)s[i]&&s[i].m(A,o);V(A,r,o),R=!0},p(A,o){if((!R||o&2)&&e!==(e=A[1].header+\"\")&&Le(T,e),o&2){n=De(A[1].questions);let i;for(i=0;i<n.length;i+=1){const _=Wr(A,n,i);s[i]?(s[i].p(_,o),m(s[i],1)):(s[i]=xr(_),s[i].c(),m(s[i],1),s[i].m(r.parentNode,r))}for(Ge(),i=n.length;i<s.length;i+=1)S(i);ge()}},i(A){if(!R){for(let o=0;o<n.length;o+=1)m(s[o]);R=!0}},o(A){s=s.filter(Boolean);for(let o=0;o<s.length;o+=1)y(s[o]);R=!1},d(A){A&&(Y(T),Y(t),Y(r)),nE(s,A)}}}function _i(E){let e,T;return e=new aE({props:{$$slots:{default:[li]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&67108866&&(R.$$scope={dirty:r,ctx:t}),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Li(E){let e,T;return e=new Ze({props:{$$slots:{default:[Fi]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&67108872&&(R.$$scope={dirty:r,ctx:t}),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Ci(E){let e,T;return e=new Ze({props:{$$slots:{default:[Vi]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&67108888&&(R.$$scope={dirty:r,ctx:t}),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function ui(E){let e,T;return e=new Ze({props:{$$slots:{default:[Wi]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&67108872&&(R.$$scope={dirty:r,ctx:t}),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function ci(E){let e,T;return e=new Ze({props:{$$slots:{default:[wi]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&67108872&&(R.$$scope={dirty:r,ctx:t}),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function fi(E){let e,T;return e=new Ze({props:{$$slots:{default:[xi]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&67108872&&(R.$$scope={dirty:r,ctx:t}),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Pi(E){let e,T;return e=new WE({props:{message:\"Put your SQL here\",$$slots:{default:[Xi]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&67108864&&(R.$$scope={dirty:r,ctx:t}),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Di(E){let e,T,t,r,R,n,s,S,A,o;e=new WE({props:{message:E[18].question}}),t=new Ze({props:{$$slots:{default:[Ji]},$$scope:{ctx:E}}}),R=new Ze({props:{$$slots:{default:[qi]},$$scope:{ctx:E}}}),s=new Ze({props:{$$slots:{default:[Qi]},$$scope:{ctx:E}}});let i=E[18].summary&&Xr(E);return{c(){K(e.$$.fragment),T=$(),K(t.$$.fragment),r=$(),K(R.$$.fragment),n=$(),K(s.$$.fragment),S=$(),i&&i.c(),A=je()},m(_,c){X(e,_,c),V(_,T,c),X(t,_,c),V(_,r,c),X(R,_,c),V(_,n,c),X(s,_,c),V(_,S,c),i&&i.m(_,c),V(_,A,c),o=!0},p(_,c){const P={};c&8&&(P.message=_[18].question),e.$set(P);const p={};c&67108872&&(p.$$scope={dirty:c,ctx:_}),t.$set(p);const C={};c&67108872&&(C.$$scope={dirty:c,ctx:_}),R.$set(C);const L={};c&67108872&&(L.$$scope={dirty:c,ctx:_}),s.$set(L),_[18].summary?i?(i.p(_,c),c&8&&m(i,1)):(i=Xr(_),i.c(),m(i,1),i.m(A.parentNode,A)):i&&(Ge(),y(i,1,1,()=>{i=null}),ge())},i(_){o||(m(e.$$.fragment,_),m(t.$$.fragment,_),m(R.$$.fragment,_),m(s.$$.fragment,_),m(i),o=!0)},o(_){y(e.$$.fragment,_),y(t.$$.fragment,_),y(R.$$.fragment,_),y(s.$$.fragment,_),y(i),o=!1},d(_){_&&(Y(T),Y(r),Y(n),Y(S),Y(A)),k(e,_),k(t,_),k(R,_),k(s,_),i&&i.d(_)}}}function di(E){let e,T;return e=new Ze({props:{$$slots:{default:[zi]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&67108873&&(R.$$scope={dirty:r,ctx:t}),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function pi(E){let e,T;return e=new Ze({props:{$$slots:{default:[ea]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&67108872&&(R.$$scope={dirty:r,ctx:t}),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Mi(E){let e,T;return e=new WE({props:{message:\"No, the results were not correct.\"}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p:j,i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Ui(E){let e,T;return e=new WE({props:{message:\"Yes, the results were correct.\"}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p:j,i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function mi(E){let e,T,t=E[0].ask_results_correct&&Kr(E);return{c(){t&&t.c(),e=je()},m(r,R){t&&t.m(r,R),V(r,e,R),T=!0},p(r,R){r[0].ask_results_correct?t?(t.p(r,R),R&1&&m(t,1)):(t=Kr(r),t.c(),m(t,1),t.m(e.parentNode,e)):t&&(Ge(),y(t,1,1,()=>{t=null}),ge())},i(r){T||(m(t),T=!0)},o(r){y(t),T=!1},d(r){r&&Y(e),t&&t.d(r)}}}function hi(E){let e,T,t=E[0].ask_results_correct&&qr(E);return{c(){t&&t.c(),e=je()},m(r,R){t&&t.m(r,R),V(r,e,R),T=!0},p(r,R){r[0].ask_results_correct?t?R&1&&m(t,1):(t=qr(r),t.c(),m(t,1),t.m(e.parentNode,e)):t&&(Ge(),y(t,1,1,()=>{t=null}),ge())},i(r){T||(m(t),T=!0)},o(r){y(t),T=!1},d(r){r&&Y(e),t&&t.d(r)}}}function Gi(E){let e,T;return e=new WE({props:{message:\"Change the chart based on these instructions\",$$slots:{default:[Ra]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&67108864&&(R.$$scope={dirty:r,ctx:t}),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function gi(E){let e,T,t=E[0].chart&&Qr(E);return{c(){t&&t.c(),e=je()},m(r,R){t&&t.m(r,R),V(r,e,R),T=!0},p(r,R){r[0].chart?t?(t.p(r,R),R&1&&m(t,1)):(t=Qr(r),t.c(),m(t,1),t.m(e.parentNode,e)):t&&(Ge(),y(t,1,1,()=>{t=null}),ge())},i(r){T||(m(t),T=!0)},o(r){y(t),T=!1},d(r){r&&Y(e),t&&t.d(r)}}}function Hi(E){let e,T,t=E[0].table&&jr(E);return{c(){t&&t.c(),e=je()},m(r,R){t&&t.m(r,R),V(r,e,R),T=!0},p(r,R){r[0].table?t?(t.p(r,R),R&1&&m(t,1)):(t=jr(r),t.c(),m(t,1),t.m(e.parentNode,e)):t&&(Ge(),y(t,1,1,()=>{t=null}),ge())},i(r){T||(m(t),T=!0)},o(r){y(t),T=!1},d(r){r&&Y(e),t&&t.d(r)}}}function bi(E){let e,T;return e=new Ze({props:{$$slots:{default:[oa]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&67108872&&(R.$$scope={dirty:r,ctx:t}),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function yi(E){let e,T,t=E[0].sql==!0&&eR(E);return{c(){t&&t.c(),e=je()},m(r,R){t&&t.m(r,R),V(r,e,R),T=!0},p(r,R){r[0].sql==!0?t?(t.p(r,R),R&1&&m(t,1)):(t=eR(r),t.c(),m(t,1),t.m(e.parentNode,e)):t&&(Ge(),y(t,1,1,()=>{t=null}),ge())},i(r){T||(m(t),T=!0)},o(r){y(t),T=!1},d(r){r&&Y(e),t&&t.d(r)}}}function Bi(E){let e,T;return e=new WE({props:{message:E[18].question}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&8&&(R.message=t[18].question),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function vi(E){let e=JSON.stringify(E[18])+\"\",T;return{c(){T=te(e)},m(t,r){V(t,T,r)},p(t,r){r&8&&e!==(e=JSON.stringify(t[18])+\"\")&&Le(T,e)},d(t){t&&Y(T)}}}function Fi(E){let e,T;return e=new aE({props:{$$slots:{default:[vi]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&67108872&&(R.$$scope={dirty:r,ctx:t}),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Yi(E){let e,T=E[18].question+\"\",t;return{c(){e=te(\"I interpreted your question as: \"),t=te(T)},m(r,R){V(r,e,R),V(r,t,R)},p(r,R){R&8&&T!==(T=r[18].question+\"\")&&Le(t,T)},d(r){r&&(Y(e),Y(t))}}}function Vi(E){let e,T,t,r,R,n;e=new aE({props:{$$slots:{default:[Yi]},$$scope:{ctx:E}}});function s(){return E[12](E[18])}return t=new IE({props:{message:\"Edit Question\",onSubmit:s}}),R=new IE({props:{message:\"New Question\",onSubmit:E[13]}}),{c(){K(e.$$.fragment),T=$(),K(t.$$.fragment),r=$(),K(R.$$.fragment)},m(S,A){X(e,S,A),V(S,T,A),X(t,S,A),V(S,r,A),X(R,S,A),n=!0},p(S,A){E=S;const o={};A&67108872&&(o.$$scope={dirty:A,ctx:E}),e.$set(o);const i={};A&24&&(i.onSubmit=s),t.$set(i)},i(S){n||(m(e.$$.fragment,S),m(t.$$.fragment,S),m(R.$$.fragment,S),n=!0)},o(S){y(e.$$.fragment,S),y(t.$$.fragment,S),y(R.$$.fragment,S),n=!1},d(S){S&&(Y(T),Y(r)),k(e,S),k(t,S),k(R,S)}}}function Wi(E){let e,T;return e=new en({props:{functionTemplate:E[18].function_template}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&8&&(R.functionTemplate=t[18].function_template),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function wi(E){let e,T;return e=new Oi({props:{functionData:E[18].function}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&8&&(R.functionData=t[18].function),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function $i(E){let e=E[18].text+\"\",T;return{c(){T=te(e)},m(t,r){V(t,T,r)},p(t,r){r&8&&e!==(e=t[18].text+\"\")&&Le(T,e)},d(t){t&&Y(T)}}}function xi(E){let e,T,t,r;return e=new aE({props:{$$slots:{default:[$i]},$$scope:{ctx:E}}}),t=new qA({props:{message:E[18].text}}),{c(){K(e.$$.fragment),T=$(),K(t.$$.fragment)},m(R,n){X(e,R,n),V(R,T,n),X(t,R,n),r=!0},p(R,n){const s={};n&67108872&&(s.$$scope={dirty:n,ctx:R}),e.$set(s);const S={};n&8&&(S.message=R[18].text),t.$set(S)},i(R){r||(m(e.$$.fragment,R),m(t.$$.fragment,R),r=!0)},o(R){y(e.$$.fragment,R),y(t.$$.fragment,R),r=!1},d(R){R&&Y(T),k(e,R),k(t,R)}}}function Xi(E){let e,T;return e=new $A({props:{onSubmit:vn}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p:j,i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function ki(E){let e=E[18].sql+\"\",T;return{c(){T=te(e)},m(t,r){V(t,T,r)},p(t,r){r&8&&e!==(e=t[18].sql+\"\")&&Le(T,e)},d(t){t&&Y(T)}}}function Ki(E){let e,T;return e=new bR({props:{$$slots:{default:[ki]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&67108872&&(R.$$scope={dirty:r,ctx:t}),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Ji(E){let e,T;return e=new aE({props:{$$slots:{default:[Ki]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&67108872&&(R.$$scope={dirty:r,ctx:t}),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function qi(E){let e,T;return e=new gR({props:{id:E[18].id,df:E[18].df}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&8&&(R.id=t[18].id),r&8&&(R.df=t[18].df),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Qi(E){let e,T;return e=new HR({props:{fig:E[18].fig}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&8&&(R.fig=t[18].fig),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Xr(E){let e,T;return e=new Ze({props:{$$slots:{default:[ji]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&67108872&&(R.$$scope={dirty:r,ctx:t}),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Zi(E){let e=E[18].summary+\"\",T;return{c(){T=te(e)},m(t,r){V(t,T,r)},p(t,r){r&8&&e!==(e=t[18].summary+\"\")&&Le(T,e)},d(t){t&&Y(T)}}}function ji(E){let e,T;return e=new aE({props:{$$slots:{default:[Zi]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&67108872&&(R.$$scope={dirty:r,ctx:t}),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function kr(E){let e,T;function t(){return E[11](E[18])}return e=new IE({props:{message:\"Auto Fix\",onSubmit:t}}),{c(){K(e.$$.fragment)},m(r,R){X(e,r,R),T=!0},p(r,R){E=r;const n={};R&8&&(n.onSubmit=t),e.$set(n)},i(r){T||(m(e.$$.fragment,r),T=!0)},o(r){y(e.$$.fragment,r),T=!1},d(r){k(e,r)}}}function zi(E){let e,T,t,r,R,n;e=new PT({props:{message:E[18].error}}),t=new IE({props:{message:\"Manually Fix\",onSubmit:E[10]}});let s=E[0].auto_fix_sql&&kr(E);return{c(){K(e.$$.fragment),T=$(),K(t.$$.fragment),r=$(),s&&s.c(),R=je()},m(S,A){X(e,S,A),V(S,T,A),X(t,S,A),V(S,r,A),s&&s.m(S,A),V(S,R,A),n=!0},p(S,A){const o={};A&8&&(o.message=S[18].error),e.$set(o),S[0].auto_fix_sql?s?(s.p(S,A),A&1&&m(s,1)):(s=kr(S),s.c(),m(s,1),s.m(R.parentNode,R)):s&&(Ge(),y(s,1,1,()=>{s=null}),ge())},i(S){n||(m(e.$$.fragment,S),m(t.$$.fragment,S),m(s),n=!0)},o(S){y(e.$$.fragment,S),y(t.$$.fragment,S),y(s),n=!1},d(S){S&&(Y(T),Y(r),Y(R)),k(e,S),k(t,S),s&&s.d(S)}}}function ea(E){let e,T;return e=new PT({props:{message:E[18].error}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&8&&(R.message=t[18].error),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Kr(E){let e,T;return e=new WE({props:{message:\"\",$$slots:{default:[ta]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&67108865&&(R.$$scope={dirty:r,ctx:t}),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Ea(E){let e,T,t,r=E[0].function_generation&&Jr(E);return T=new IE({props:{message:\"Yes, train as Question-SQL pair\",onSubmit:E[8]}}),{c(){r&&r.c(),e=$(),K(T.$$.fragment)},m(R,n){r&&r.m(R,n),V(R,e,n),X(T,R,n),t=!0},p(R,n){R[0].function_generation?r?n&1&&m(r,1):(r=Jr(R),r.c(),m(r,1),r.m(e.parentNode,e)):r&&(Ge(),y(r,1,1,()=>{r=null}),ge())},i(R){t||(m(r),m(T.$$.fragment,R),t=!0)},o(R){y(r),y(T.$$.fragment,R),t=!1},d(R){R&&Y(e),r&&r.d(R),k(T,R)}}}function Jr(E){let e,T;return e=new IE({props:{message:\"Yes, create function\",onSubmit:E[7]}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function ta(E){let e=fT()!==null,T,t,r,R=e&&Ea(E);return t=new IE({props:{message:\"No\",onSubmit:E[9]}}),{c(){R&&R.c(),T=$(),K(t.$$.fragment)},m(n,s){R&&R.m(n,s),V(n,T,s),X(t,n,s),r=!0},p(n,s){e&&R.p(n,s)},i(n){r||(m(R),m(t.$$.fragment,n),r=!0)},o(n){y(R),y(t.$$.fragment,n),r=!1},d(n){n&&Y(T),R&&R.d(n),k(t,n)}}}function qr(E){let e,T;return e=new Ze({props:{$$slots:{default:[ra]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Ta(E){let e;return{c(){e=te(\"Were the results correct?\")},m(T,t){V(T,e,t)},d(T){T&&Y(e)}}}function ra(E){let e,T;return e=new aE({props:{$$slots:{default:[Ta]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&67108864&&(R.$$scope={dirty:r,ctx:t}),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Ra(E){let e,T;return e=new kA({props:{onSubmit:E[6],placeholder:\"Make the line red\",buttonText:\"Update Chart\",currentValue:\"\"}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p:j,i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Qr(E){let e,T,t,r;e=new Ze({props:{$$slots:{default:[na]},$$scope:{ctx:E}}});let R=E[0].redraw_chart&&Zr(E);return{c(){K(e.$$.fragment),T=$(),R&&R.c(),t=je()},m(n,s){X(e,n,s),V(n,T,s),R&&R.m(n,s),V(n,t,s),r=!0},p(n,s){const S={};s&67108872&&(S.$$scope={dirty:s,ctx:n}),e.$set(S),n[0].redraw_chart?R?s&1&&m(R,1):(R=Zr(n),R.c(),m(R,1),R.m(t.parentNode,t)):R&&(Ge(),y(R,1,1,()=>{R=null}),ge())},i(n){r||(m(e.$$.fragment,n),m(R),r=!0)},o(n){y(e.$$.fragment,n),y(R),r=!1},d(n){n&&(Y(T),Y(t)),k(e,n),R&&R.d(n)}}}function na(E){let e,T;return e=new HR({props:{fig:E[18].fig}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&8&&(R.fig=t[18].fig),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Zr(E){let e,T;return e=new Ze({props:{$$slots:{default:[Aa]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Aa(E){let e,T;return e=new IE({props:{message:\"Redraw Chart\",onSubmit:Fn}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p:j,i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function jr(E){let e,T;return e=new Ze({props:{$$slots:{default:[sa]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&67108872&&(R.$$scope={dirty:r,ctx:t}),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function sa(E){let e,T;return e=new gR({props:{id:E[18].id,df:E[18].df}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&8&&(R.id=t[18].id),r&8&&(R.df=t[18].df),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function zr(E){let e,T;return e=new IE({props:{message:E[21],onSubmit:uT}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&8&&(R.message=t[21]),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Sa(E){let e=E[18].header+\"\",T,t,r,R,n=De(E[18].questions),s=[];for(let A=0;A<n.length;A+=1)s[A]=zr(Vr(E,n,A));const S=A=>y(s[A],1,1,()=>{s[A]=null});return{c(){T=te(e),t=$();for(let A=0;A<s.length;A+=1)s[A].c();r=je()},m(A,o){V(A,T,o),V(A,t,o);for(let i=0;i<s.length;i+=1)s[i]&&s[i].m(A,o);V(A,r,o),R=!0},p(A,o){if((!R||o&8)&&e!==(e=A[18].header+\"\")&&Le(T,e),o&8){n=De(A[18].questions);let i;for(i=0;i<n.length;i+=1){const _=Vr(A,n,i);s[i]?(s[i].p(_,o),m(s[i],1)):(s[i]=zr(_),s[i].c(),m(s[i],1),s[i].m(r.parentNode,r))}for(Ge(),i=n.length;i<s.length;i+=1)S(i);ge()}},i(A){if(!R){for(let o=0;o<n.length;o+=1)m(s[o]);R=!0}},o(A){s=s.filter(Boolean);for(let o=0;o<s.length;o+=1)y(s[o]);R=!1},d(A){A&&(Y(T),Y(t),Y(r)),nE(s,A)}}}function oa(E){let e,T;return e=new aE({props:{$$slots:{default:[Sa]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&67108872&&(R.$$scope={dirty:r,ctx:t}),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function eR(E){let e,T;return e=new Ze({props:{$$slots:{default:[aa]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&67108872&&(R.$$scope={dirty:r,ctx:t}),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Oa(E){let e=E[18].text+\"\",T;return{c(){T=te(e)},m(t,r){V(t,T,r)},p(t,r){r&8&&e!==(e=t[18].text+\"\")&&Le(T,e)},d(t){t&&Y(T)}}}function ia(E){let e,T;return e=new bR({props:{$$slots:{default:[Oa]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&67108872&&(R.$$scope={dirty:r,ctx:t}),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function aa(E){let e,T;return e=new aE({props:{$$slots:{default:[ia]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&67108872&&(R.$$scope={dirty:r,ctx:t}),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function ER(E){let e,T,t,r;const R=[Bi,yi,bi,Hi,gi,Gi,hi,mi,Ui,Mi,pi,di,Di,Pi,fi,ci,ui,Ci,Li],n=[];function s(S,A){return S[18].type===\"user_question\"?0:S[18].type===\"sql\"?1:S[18].type===\"question_list\"?2:S[18].type===\"df\"?3:S[18].type===\"plotly_figure\"?4:S[18].type===\"chart_modification\"?5:S[18].type===\"feedback_question\"?6:S[18].type===\"feedback_buttons\"?7:S[18].type===\"feedback_correct\"?8:S[18].type===\"feedback_incorrect\"?9:S[18].type===\"error\"?10:S[18].type===\"sql_error\"?11:S[18].type===\"question_cache\"?12:S[18].type===\"user_sql\"?13:S[18].type===\"text\"?14:S[18].type===\"function\"?15:S[18].type===\"function_template\"?16:S[18].type===\"rewritten_question\"?17:18}return e=s(E),T=n[e]=R[e](E),{c(){T.c(),t=je()},m(S,A){n[e].m(S,A),V(S,t,A),r=!0},p(S,A){let o=e;e=s(S),e===o?n[e].p(S,A):(Ge(),y(n[o],1,1,()=>{n[o]=null}),ge(),T=n[e],T?T.p(S,A):(T=n[e]=R[e](S),T.c()),m(T,1),T.m(t.parentNode,t))},i(S){r||(m(T),r=!0)},o(S){y(T),r=!1},d(S){S&&Y(t),n[e].d(S)}}}function tR(E){let e,T;return e=new dA({}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function TR(E){let e,T,t=De(E[3]),r=[];for(let n=0;n<t.length;n+=1)r[n]=RR(Fr(E,t,n));const R=n=>y(r[n],1,1,()=>{r[n]=null});return{c(){for(let n=0;n<r.length;n+=1)r[n].c();e=je()},m(n,s){for(let S=0;S<r.length;S+=1)r[S]&&r[S].m(n,s);V(n,e,s),T=!0},p(n,s){if(s&8){t=De(n[3]);let S;for(S=0;S<t.length;S+=1){const A=Fr(n,t,S);r[S]?(r[S].p(A,s),m(r[S],1)):(r[S]=RR(A),r[S].c(),m(r[S],1),r[S].m(e.parentNode,e))}for(Ge(),S=t.length;S<r.length;S+=1)R(S);ge()}},i(n){if(!T){for(let s=0;s<t.length;s+=1)m(r[s]);T=!0}},o(n){r=r.filter(Boolean);for(let s=0;s<r.length;s+=1)y(r[s]);T=!1},d(n){n&&Y(e),nE(r,n)}}}function rR(E){let e,T;function t(){return E[14](E[15])}return e=new yA({props:{message:\"Re-Run SQL\",onSubmit:t}}),{c(){K(e.$$.fragment)},m(r,R){X(e,r,R),T=!0},p(r,R){E=r;const n={};R&8&&(n.onSubmit=t),e.$set(n)},i(r){T||(m(e.$$.fragment,r),T=!0)},o(r){y(e.$$.fragment,r),T=!1},d(r){k(e,r)}}}function RR(E){let e,T,t=E[15].type===\"question_cache\"&&rR(E);return{c(){t&&t.c(),e=je()},m(r,R){t&&t.m(r,R),V(r,e,R),T=!0},p(r,R){r[15].type===\"question_cache\"?t?(t.p(r,R),R&8&&m(t,1)):(t=rR(r),t.c(),m(t,1),t.m(e.parentNode,e)):t&&(Ge(),y(t,1,1,()=>{t=null}),ge())},i(r){T||(m(t),T=!0)},o(r){y(t),T=!1},d(r){r&&Y(e),t&&t.d(r)}}}function Ia(E){let e,T,t,r,R,n,s,S,A,o,i,_,c,P,p;t=new EA({});let C=E[0].debug&&wr(),L=E[1]&&E[1].type==\"question_list\"&&!E[2]&&$r(E),I=De(E[3]),u=[];for(let O=0;O<I.length;O+=1)u[O]=ER(Yr(E,I,O));const H=O=>y(u[O],1,1,()=>{u[O]=null});let b=E[5]&&tR();i=new lA({});let M=E[2]&&TR(E);return P=new IA({props:{onSubmit:uT}}),{c(){e=f(\"div\"),T=f(\"div\"),K(t.$$.fragment),r=$(),C&&C.c(),R=$(),L&&L.c(),n=$(),s=f(\"ul\");for(let O=0;O<u.length;O+=1)u[O].c();S=$(),b&&b.c(),A=$(),o=f(\"footer\"),K(i.$$.fragment),_=$(),M&&M.c(),c=$(),K(P.$$.fragment),a(s,\"class\",\"mt-16 space-y-5\"),a(T,\"class\",\"py-10 lg:py-14\"),a(o,\"class\",\"max-w-4xl mx-auto sticky bottom-0 z-10 p-3 sm:py-6\"),a(e,\"class\",\"relative h-screen w-full lg:pl-64\")},m(O,N){V(O,e,N),l(e,T),X(t,T,null),l(T,r),C&&C.m(T,null),l(T,R),L&&L.m(T,null),l(T,n),l(T,s);for(let D=0;D<u.length;D+=1)u[D]&&u[D].m(s,null);l(s,S),b&&b.m(s,null),l(e,A),l(e,o),X(i,o,null),l(o,_),M&&M.m(o,null),l(o,c),X(P,o,null),p=!0},p(O,[N]){if(O[0].debug?C?N&1&&m(C,1):(C=wr(),C.c(),m(C,1),C.m(T,R)):C&&(Ge(),y(C,1,1,()=>{C=null}),ge()),O[1]&&O[1].type==\"question_list\"&&!O[2]?L?(L.p(O,N),N&6&&m(L,1)):(L=$r(O),L.c(),m(L,1),L.m(T,n)):L&&(Ge(),y(L,1,1,()=>{L=null}),ge()),N&25){I=De(O[3]);let D;for(D=0;D<I.length;D+=1){const B=Yr(O,I,D);u[D]?(u[D].p(B,N),m(u[D],1)):(u[D]=ER(B),u[D].c(),m(u[D],1),u[D].m(s,S))}for(Ge(),D=I.length;D<u.length;D+=1)H(D);ge()}O[5]?b?N&32&&m(b,1):(b=tR(),b.c(),m(b,1),b.m(s,null)):b&&(Ge(),y(b,1,1,()=>{b=null}),ge()),O[2]?M?(M.p(O,N),N&4&&m(M,1)):(M=TR(O),M.c(),m(M,1),M.m(o,c)):M&&(Ge(),y(M,1,1,()=>{M=null}),ge())},i(O){if(!p){m(t.$$.fragment,O),m(C),m(L);for(let N=0;N<I.length;N+=1)m(u[N]);m(b),m(i.$$.fragment,O),m(M),m(P.$$.fragment,O),p=!0}},o(O){y(t.$$.fragment,O),y(C),y(L),u=u.filter(Boolean);for(let N=0;N<u.length;N+=1)y(u[N]);y(b),y(i.$$.fragment,O),y(M),y(P.$$.fragment,O),p=!1},d(O){O&&Y(e),k(t),C&&C.d(),L&&L.d(),nE(u,O),b&&b.d(),k(i),M&&M.d(),k(P)}}}function Na(E,e,T){let t,r,R,n,s,S;return eE(E,VE,I=>T(0,t=I)),eE(E,LT,I=>T(1,r=I)),eE(E,Ht,I=>T(2,R=I)),eE(E,YE,I=>T(3,n=I)),eE(E,BE,I=>T(4,s=I)),eE(E,St,I=>T(5,S=I)),[t,r,R,n,s,S,I=>{$n(I)},()=>{Yn()},()=>{wn()},()=>{wT()},()=>{wT()},I=>{Bn(I.error)},I=>{it(),OT(BE,s=I.question,s)},()=>{it()},I=>I.type===\"question_cache\"?dn(I.id):void 0]}class la extends ue{constructor(e){super(),Ce(this,e,Na,Ia,_e,{})}}function _a(E){let e,T,t,r,R,n,s,S,A,o,i,_,c,P,p,C,L,I,u;return{c(){e=f(\"div\"),T=f(\"div\"),t=f(\"div\"),r=f(\"div\"),R=f(\"h3\"),R.textContent=\"Are you sure?\",n=$(),s=f(\"button\"),s.innerHTML='<span class=\"sr-only\">Close</span> <svg class=\"w-3.5 h-3.5\" width=\"8\" height=\"8\" viewBox=\"0 0 8 8\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M0.258206 1.00652C0.351976 0.912791 0.479126 0.860131 0.611706 0.860131C0.744296 0.860131 0.871447 0.912791 0.965207 1.00652L3.61171 3.65302L6.25822 1.00652C6.30432 0.958771 6.35952 0.920671 6.42052 0.894471C6.48152 0.868271 6.54712 0.854471 6.61352 0.853901C6.67992 0.853321 6.74572 0.865971 6.80722 0.891111C6.86862 0.916251 6.92442 0.953381 6.97142 1.00032C7.01832 1.04727 7.05552 1.1031 7.08062 1.16454C7.10572 1.22599 7.11842 1.29183 7.11782 1.35822C7.11722 1.42461 7.10342 1.49022 7.07722 1.55122C7.05102 1.61222 7.01292 1.6674 6.96522 1.71352L4.31871 4.36002L6.96522 7.00648C7.05632 7.10078 7.10672 7.22708 7.10552 7.35818C7.10442 7.48928 7.05182 7.61468 6.95912 7.70738C6.86642 7.80018 6.74102 7.85268 6.60992 7.85388C6.47882 7.85498 6.35252 7.80458 6.25822 7.71348L3.61171 5.06702L0.965207 7.71348C0.870907 7.80458 0.744606 7.85498 0.613506 7.85388C0.482406 7.85268 0.357007 7.80018 0.264297 7.70738C0.171597 7.61468 0.119017 7.48928 0.117877 7.35818C0.116737 7.22708 0.167126 7.10078 0.258206 7.00648L2.90471 4.36002L0.258206 1.71352C0.164476 1.61976 0.111816 1.4926 0.111816 1.36002C0.111816 1.22744 0.164476 1.10028 0.258206 1.00652Z\" fill=\"currentColor\"></path></svg>',S=$(),A=f(\"div\"),o=f(\"p\"),i=te(E[0]),_=$(),c=f(\"div\"),P=f(\"button\"),P.textContent=\"Close\",p=$(),C=f(\"button\"),L=te(E[1]),a(R,\"class\",\"font-bold text-gray-800 dark:text-white\"),a(s,\"type\",\"button\"),a(s,\"class\",\"hs-dropdown-toggle inline-flex flex-shrink-0 justify-center items-center h-8 w-8 rounded-md text-gray-500 hover:text-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-white transition-all text-sm dark:focus:ring-gray-700 dark:focus:ring-offset-gray-800\"),a(s,\"data-hs-overlay\",\"#hs-vertically-centered-modal\"),a(r,\"class\",\"flex justify-between items-center py-3 px-4 border-b dark:border-gray-700\"),a(o,\"class\",\"text-gray-800 dark:text-gray-400\"),a(A,\"class\",\"p-4 overflow-y-auto\"),a(P,\"type\",\"button\"),a(P,\"class\",\"hs-dropdown-toggle py-3 px-4 inline-flex justify-center items-center gap-2 rounded-md border font-medium bg-white text-gray-700 shadow-sm align-middle hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-white focus:ring-blue-600 transition-all text-sm dark:bg-slate-900 dark:hover:bg-slate-800 dark:border-gray-700 dark:text-gray-400 dark:hover:text-white dark:focus:ring-offset-gray-800\"),a(P,\"data-hs-overlay\",\"#hs-vertically-centered-modal\"),a(C,\"class\",\"py-3 px-4 inline-flex justify-center items-center gap-2 rounded-md border border-transparent font-semibold bg-blue-500 text-white hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all text-sm dark:focus:ring-offset-gray-800\"),a(c,\"class\",\"flex justify-end items-center gap-x-2 py-3 px-4 border-t dark:border-gray-700\"),a(t,\"class\",\"flex flex-col bg-white border shadow-sm rounded-xl dark:bg-gray-800 dark:border-gray-700 dark:shadow-slate-700/[.7]\"),a(T,\"class\",\"hs-overlay-open:mt-7 hs-overlay-open:opacity-100 hs-overlay-open:duration-500 mt-0 opacity-0 ease-out transition-all sm:max-w-lg sm:w-full m-3 sm:mx-auto min-h-[calc(100%-3.5rem)] flex items-center\"),a(e,\"class\",\"hs-overlay open w-full h-full fixed top-0 left-0 z-[60] overflow-x-hidden overflow-y-auto\")},m(H,b){V(H,e,b),l(e,T),l(T,t),l(t,r),l(r,R),l(r,n),l(r,s),l(t,S),l(t,A),l(A,o),l(o,i),l(t,_),l(t,c),l(c,P),l(c,p),l(c,C),l(C,L),I||(u=[Ne(s,\"click\",function(){zE(E[2])&&E[2].apply(this,arguments)}),Ne(P,\"click\",function(){zE(E[2])&&E[2].apply(this,arguments)}),Ne(C,\"click\",function(){zE(E[3])&&E[3].apply(this,arguments)})],I=!0)},p(H,[b]){E=H,b&1&&Le(i,E[0]),b&2&&Le(L,E[1])},i:j,o:j,d(H){H&&Y(e),I=!1,NE(u)}}}function La(E,e,T){let{message:t}=e,{buttonLabel:r}=e,{onClose:R}=e,{onConfirm:n}=e;return E.$$set=s=>{\"message\"in s&&T(0,t=s.message),\"buttonLabel\"in s&&T(1,r=s.buttonLabel),\"onClose\"in s&&T(2,R=s.onClose),\"onConfirm\"in s&&T(3,n=s.onConfirm)},[t,r,R,n]}class Ca extends ue{constructor(e){super(),Ce(this,e,La,_a,_e,{message:0,buttonLabel:1,onClose:2,onConfirm:3})}}function nR(E,e,T){const t=E.slice();return t[10]=e[T].name,t[11]=e[T].description,t[12]=e[T].example,t}function AR(E){let e,T,t,r,R,n,s,S,A,o,i,_;return o=Sn(E[7][0]),{c(){e=f(\"div\"),T=f(\"div\"),t=f(\"input\"),r=$(),R=f(\"label\"),n=f(\"span\"),n.textContent=`${E[10]}`,s=$(),S=f(\"span\"),S.textContent=`${E[11]}`,A=$(),a(t,\"id\",\"hs-radio-\"+E[10]),t.__value=E[10],Ye(t,t.__value),a(t,\"name\",\"hs-radio-with-description\"),a(t,\"type\",\"radio\"),a(t,\"class\",\"border-gray-200 rounded-full text-blue-600 focus:ring-blue-500 dark:bg-gray-800 dark:border-gray-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800\"),a(t,\"aria-describedby\",\"hs-radio-delete-description\"),a(T,\"class\",\"flex items-center h-5 mt-1\"),a(n,\"class\",\"block text-sm font-semibold text-gray-800 dark:text-gray-300\"),a(S,\"id\",\"hs-radio-ddl-description\"),a(S,\"class\",\"block text-sm text-gray-600 dark:text-gray-500\"),a(R,\"for\",\"hs-radio-\"+E[10]),a(R,\"class\",\"ml-3\"),a(e,\"class\",\"relative flex items-start\"),o.p(t)},m(c,P){V(c,e,P),l(e,T),l(T,t),t.checked=t.__value===E[0],l(e,r),l(e,R),l(R,n),l(R,s),l(R,S),l(e,A),i||(_=Ne(t,\"change\",E[6]),i=!0)},p(c,P){P&1&&(t.checked=t.__value===c[0])},d(c){c&&Y(e),o.r(),i=!1,_()}}}function ua(E){let e,T,t,r,R,n,s,S,A,o,i,_,c,P,p,C,L,I,u,H,b,M,O,N,D,B=De(E[3]),h=[];for(let G=0;G<B.length;G+=1)h[G]=AR(nR(E,B,G));return{c(){var G;e=f(\"div\"),T=f(\"div\"),t=f(\"div\"),r=f(\"div\"),R=f(\"h2\"),R.textContent=\"Add Training Data\",n=$(),s=f(\"button\"),s.innerHTML='<span class=\"sr-only\">Close</span> <svg class=\"w-3.5 h-3.5\" width=\"8\" height=\"8\" viewBox=\"0 0 8 8\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M0.258206 1.00652C0.351976 0.912791 0.479126 0.860131 0.611706 0.860131C0.744296 0.860131 0.871447 0.912791 0.965207 1.00652L3.61171 3.65302L6.25822 1.00652C6.30432 0.958771 6.35952 0.920671 6.42052 0.894471C6.48152 0.868271 6.54712 0.854471 6.61352 0.853901C6.67992 0.853321 6.74572 0.865971 6.80722 0.891111C6.86862 0.916251 6.92442 0.953381 6.97142 1.00032C7.01832 1.04727 7.05552 1.1031 7.08062 1.16454C7.10572 1.22599 7.11842 1.29183 7.11782 1.35822C7.11722 1.42461 7.10342 1.49022 7.07722 1.55122C7.05102 1.61222 7.01292 1.6674 6.96522 1.71352L4.31871 4.36002L6.96522 7.00648C7.05632 7.10078 7.10672 7.22708 7.10552 7.35818C7.10442 7.48928 7.05182 7.61468 6.95912 7.70738C6.86642 7.80018 6.74102 7.85268 6.60992 7.85388C6.47882 7.85498 6.35252 7.80458 6.25822 7.71348L3.61171 5.06702L0.965207 7.71348C0.870907 7.80458 0.744606 7.85498 0.613506 7.85388C0.482406 7.85268 0.357007 7.80018 0.264297 7.70738C0.171597 7.61468 0.119017 7.48928 0.117877 7.35818C0.116737 7.22708 0.167126 7.10078 0.258206 7.00648L2.90471 4.36002L0.258206 1.71352C0.164476 1.61976 0.111816 1.4926 0.111816 1.36002C0.111816 1.22744 0.164476 1.10028 0.258206 1.00652Z\" fill=\"currentColor\"></path></svg>',S=$(),A=f(\"span\"),A.textContent=\"Training Data Type\",o=$(),i=f(\"div\");for(let F=0;F<h.length;F+=1)h[F].c();_=$(),c=f(\"div\"),P=f(\"label\"),p=te(\"Your \"),C=te(E[0]),L=$(),I=f(\"div\"),u=f(\"textarea\"),b=$(),M=f(\"div\"),O=f(\"button\"),O.textContent=\"Save\",a(R,\"class\",\"text-xl text-gray-800 font-bold sm:text-3xl dark:text-white\"),a(s,\"type\",\"button\"),a(s,\"class\",\"hs-dropdown-toggle inline-flex flex-shrink-0 justify-center items-center h-8 w-8 rounded-md text-gray-500 hover:text-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-white transition-all text-sm dark:focus:ring-gray-700 dark:focus:ring-offset-gray-800\"),a(s,\"data-hs-overlay\",\"#hs-vertically-centered-modal\"),a(r,\"class\",\"flex justify-between items-center py-3 px-4 border-b dark:border-gray-700 mb-2\"),a(A,\"class\",\"block mb-2 text-sm font-medium dark:text-white\"),a(i,\"class\",\"grid space-y-3 mb-1\"),a(P,\"for\",\"hs-feedback-post-comment-textarea-1\"),a(P,\"class\",\"block mt-2 mb-2 text-sm font-medium dark:text-white\"),a(u,\"id\",\"hs-feedback-post-comment-textarea-1\"),a(u,\"name\",\"hs-feedback-post-comment-textarea-1\"),a(u,\"rows\",\"3\"),a(u,\"class\",\"py-3 px-4 block w-full border border-gray-200 rounded-md text-sm focus:border-blue-500 focus:ring-blue-500 sm:p-4 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400\"),a(u,\"placeholder\",H=((G=E[3].find(E[8]))==null?void 0:G.example)??\"No example available\"),a(I,\"class\",\"mt-1\"),a(c,\"class\",\"mt-2 border-t dark:border-gray-700\"),a(O,\"class\",\"py-3 px-4 inline-flex justify-center items-center gap-2 rounded-md border border-transparent font-semibold bg-blue-500 text-white hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all dark:focus:ring-offset-gray-800\"),a(M,\"class\",\"mt-6 grid\"),a(t,\"class\",\"mt-5 p-4 relative z-10 bg-white border rounded-xl sm:mt-10 md:p-10 dark:bg-gray-800 dark:border-gray-700\"),a(T,\"class\",\"mx-auto max-w-2xl\"),a(e,\"class\",\"max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 mx-auto\")},m(G,F){V(G,e,F),l(e,T),l(T,t),l(t,r),l(r,R),l(r,n),l(r,s),l(t,S),l(t,A),l(t,o),l(t,i);for(let W=0;W<h.length;W+=1)h[W]&&h[W].m(i,null);l(t,_),l(t,c),l(c,P),l(P,p),l(P,C),l(c,L),l(c,I),l(I,u),Ye(u,E[2]),l(t,b),l(t,M),l(M,O),N||(D=[Ne(s,\"click\",function(){zE(E[1])&&E[1].apply(this,arguments)}),Ne(u,\"input\",E[9]),Ne(O,\"click\",E[4])],N=!0)},p(G,[F]){var W;if(E=G,F&9){B=De(E[3]);let x;for(x=0;x<B.length;x+=1){const J=nR(E,B,x);h[x]?h[x].p(J,F):(h[x]=AR(J),h[x].c(),h[x].m(i,null))}for(;x<h.length;x+=1)h[x].d(1);h.length=B.length}F&1&&Le(C,E[0]),F&1&&H!==(H=((W=E[3].find(E[8]))==null?void 0:W.example)??\"No example available\")&&a(u,\"placeholder\",H),F&4&&Ye(u,E[2])},i:j,o:j,d(G){G&&Y(e),nE(h,G),N=!1,NE(D)}}}function ca(E,e,T){let{onDismiss:t}=e,{onTrain:r}=e,{selectedTrainingDataType:R=\"SQL\"}=e,n=[{name:\"DDL\",description:\"These are the CREATE TABLE statements that define your database structure.\",example:\"CREATE TABLE table_name (column_1 datatype, column_2 datatype, column_3 datatype);\"},{name:\"Documentation\",description:\"This can be any text-based documentation. Keep the chunks small and focused on a single topic.\",example:\"Our definition of ABC is XYZ.\"},{name:\"SQL\",description:\"This can be any SQL statement that works. The more the merrier.\",example:\"SELECT column_1, column_2 FROM table_name;\"}],s=\"\";const S=()=>{r(s,R.toLowerCase())},A=[[]];function o(){R=this.__value,T(0,R)}const i=c=>c.name===R;function _(){s=this.value,T(2,s)}return E.$$set=c=>{\"onDismiss\"in c&&T(1,t=c.onDismiss),\"onTrain\"in c&&T(5,r=c.onTrain),\"selectedTrainingDataType\"in c&&T(0,R=c.selectedTrainingDataType)},[R,t,s,n,S,r,o,A,i,_]}class fa extends ue{constructor(e){super(),Ce(this,e,ca,ua,_e,{onDismiss:1,onTrain:5,selectedTrainingDataType:0})}}function sR(E,e,T){const t=E.slice();return t[21]=e[T],t}function SR(E,e,T){const t=E.slice();return t[24]=e[T],t}function oR(E,e,T){const t=E.slice();return t[24]=e[T],t}function iR(E){let e,T;return e=new fa({props:{onDismiss:E[13],onTrain:E[0]}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&1&&(R.onTrain=t[0]),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Pa(E){let e;return{c(){e=te(\"Action\")},m(T,t){V(T,e,t)},p:j,d(T){T&&Y(e)}}}function Da(E){let e=E[24]+\"\",T;return{c(){T=te(e)},m(t,r){V(t,T,r)},p:j,d(t){t&&Y(T)}}}function aR(E){let e,T,t,r;function R(S,A){return S[24]!=\"id\"?Da:Pa}let s=R(E)(E);return{c(){e=f(\"th\"),T=f(\"div\"),t=f(\"span\"),s.c(),r=$(),a(t,\"class\",\"text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-gray-200\"),a(T,\"class\",\"flex items-center gap-x-2\"),a(e,\"scope\",\"col\"),a(e,\"class\",\"px-6 py-3 text-left\")},m(S,A){V(S,e,A),l(e,T),l(T,t),s.m(t,null),l(e,r)},p(S,A){s.p(S,A)},d(S){S&&Y(e),s.d()}}}function da(E){let e,T,t;function r(){return E[18](E[21],E[24])}return{c(){e=f(\"button\"),e.textContent=\"Delete\",a(e,\"type\",\"button\"),a(e,\"class\",\"py-2 px-3 inline-flex justify-center items-center gap-2 rounded-md border-2 border-red-200 font-semibold text-red-500 hover:text-white hover:bg-red-500 hover:border-red-500 focus:outline-none focus:ring-2 focus:ring-red-200 focus:ring-offset-2 transition-all text-sm dark:focus:ring-offset-gray-800\")},m(R,n){V(R,e,n),T||(t=Ne(e,\"click\",r),T=!0)},p(R,n){E=R},d(R){R&&Y(e),T=!1,t()}}}function pa(E){let e,T=E[21][E[24]]+\"\",t;return{c(){e=f(\"span\"),t=te(T),a(e,\"class\",\"text-gray-800 dark:text-gray-200\")},m(r,R){V(r,e,R),l(e,t)},p(r,R){R&16&&T!==(T=r[21][r[24]]+\"\")&&Le(t,T)},d(r){r&&Y(e)}}}function IR(E){let e,T;function t(n,s){return n[24]!=\"id\"?pa:da}let R=t(E)(E);return{c(){e=f(\"td\"),T=f(\"div\"),R.c(),a(T,\"class\",\"px-6 py-3\"),a(e,\"class\",\"h-px w-px \")},m(n,s){V(n,e,s),l(e,T),R.m(T,null)},p(n,s){R.p(n,s)},d(n){n&&Y(e),R.d()}}}function NR(E){let e,T,t=De(E[8]),r=[];for(let R=0;R<t.length;R+=1)r[R]=IR(SR(E,t,R));return{c(){e=f(\"tr\");for(let R=0;R<r.length;R+=1)r[R].c();T=$()},m(R,n){V(R,e,n);for(let s=0;s<r.length;s+=1)r[s]&&r[s].m(e,null);l(e,T)},p(R,n){if(n&304){t=De(R[8]);let s;for(s=0;s<t.length;s+=1){const S=SR(R,t,s);r[s]?r[s].p(S,n):(r[s]=IR(S),r[s].c(),r[s].m(e,T))}for(;s<r.length;s+=1)r[s].d(1);r.length=t.length}},d(R){R&&Y(e),nE(r,R)}}}function lR(E){let e,T;return e=new Ca({props:{message:\"Are you sure you want to delete this?\",buttonLabel:\"Delete\",onClose:E[19],onConfirm:E[20]}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&32&&(R.onClose=t[19]),r&34&&(R.onConfirm=t[20]),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Ma(E){let e,T,t,r,R,n,s,S,A,o,i,_,c,P,p,C,L,I,u,H,b,M,O,N,D,B,h,G=E[2]+1+\"\",F,W,x=Math.min(E[3],E[7].length)+\"\",J,oe,z,Oe,Ue,ye,TE,dE,me,fE,rE,pE,Tt,rt,Ve=E[6]&&iR(E),ME=De(E[8]),$e=[];for(let Te=0;Te<ME.length;Te+=1)$e[Te]=aR(oR(E,ME,Te));let ze=De(E[4]),ve=[];for(let Te=0;Te<ze.length;Te+=1)ve[Te]=NR(sR(E,ze,Te));let Fe=E[5]!=null&&lR(E);return{c(){Ve&&Ve.c(),e=$(),T=f(\"div\"),t=f(\"div\"),r=f(\"div\"),R=f(\"div\"),n=f(\"div\"),s=f(\"div\"),S=f(\"div\"),S.innerHTML='<h2 class=\"text-xl font-semibold text-gray-800 dark:text-gray-200\">Training Data</h2> <p class=\"text-sm text-gray-600 dark:text-gray-400\">Add or remove training data. Good training data is the key to accuracy.</p>',A=$(),o=f(\"div\"),i=f(\"div\"),_=f(\"button\"),_.textContent=\"View all\",c=$(),P=f(\"button\"),P.innerHTML=`<svg class=\"w-3 h-3\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\"><path d=\"M2.63452 7.50001L13.6345 7.5M8.13452 13V2\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"></path></svg>\n                    Add training data`,p=$(),C=f(\"table\"),L=f(\"thead\"),I=f(\"tr\");for(let Te=0;Te<$e.length;Te+=1)$e[Te].c();u=$(),H=f(\"tbody\");for(let Te=0;Te<ve.length;Te+=1)ve[Te].c();b=$(),M=f(\"div\"),O=f(\"div\"),N=f(\"p\"),N.textContent=\"Showing:\",D=$(),B=f(\"div\"),h=f(\"span\"),F=te(G),W=te(\" - \"),J=te(x),oe=$(),z=f(\"p\"),z.textContent=`of ${E[7].length}`,Oe=$(),Ue=f(\"div\"),ye=f(\"div\"),TE=f(\"button\"),TE.innerHTML=`<svg class=\"w-3 h-3\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z\"></path></svg>\n                    Prev`,dE=$(),me=f(\"button\"),me.innerHTML=`Next\n                    <svg class=\"w-3 h-3\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z\"></path></svg>`,fE=$(),Fe&&Fe.c(),rE=je(),a(_,\"class\",\"py-2 px-3 inline-flex justify-center items-center gap-2 rounded-md border font-medium bg-white text-gray-700 shadow-sm align-middle hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-white focus:ring-blue-600 transition-all text-sm dark:bg-slate-900 dark:hover:bg-slate-800 dark:border-gray-700 dark:text-gray-400 dark:hover:text-white dark:focus:ring-offset-gray-800\"),a(P,\"class\",\"py-2 px-3 inline-flex justify-center items-center gap-2 rounded-md border border-transparent font-semibold bg-blue-500 text-white hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all text-sm dark:focus:ring-offset-gray-800\"),a(i,\"class\",\"inline-flex gap-x-2\"),a(s,\"class\",\"px-6 py-4 grid gap-3 md:flex md:justify-between md:items-center border-b border-gray-200 dark:border-gray-700\"),a(L,\"class\",\"bg-gray-50 dark:bg-slate-800\"),a(H,\"class\",\"divide-y divide-gray-200 dark:divide-gray-700\"),a(C,\"class\",\"min-w-full divide-y divide-gray-200 dark:divide-gray-700\"),a(N,\"class\",\"text-sm text-gray-600 dark:text-gray-400\"),a(h,\"class\",\"py-2 px-3 pr-9 block w-full border-gray-200 rounded-md text-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-slate-900 dark:border-gray-700 dark:text-gray-400\"),a(B,\"class\",\"max-w-sm space-y-3\"),a(z,\"class\",\"text-sm text-gray-600 dark:text-gray-400\"),a(O,\"class\",\"inline-flex items-center gap-x-2\"),a(TE,\"type\",\"button\"),a(TE,\"class\",\"py-2 px-3 inline-flex justify-center items-center gap-2 rounded-md border font-medium bg-white text-gray-700 shadow-sm align-middle hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-white focus:ring-blue-600 transition-all text-sm dark:bg-slate-900 dark:hover:bg-slate-800 dark:border-gray-700 dark:text-gray-400 dark:hover:text-white dark:focus:ring-offset-gray-800\"),a(me,\"type\",\"button\"),a(me,\"class\",\"py-2 px-3 inline-flex justify-center items-center gap-2 rounded-md border font-medium bg-white text-gray-700 shadow-sm align-middle hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-white focus:ring-blue-600 transition-all text-sm dark:bg-slate-900 dark:hover:bg-slate-800 dark:border-gray-700 dark:text-gray-400 dark:hover:text-white dark:focus:ring-offset-gray-800\"),a(ye,\"class\",\"inline-flex gap-x-2\"),a(M,\"class\",\"px-6 py-4 grid gap-3 md:flex md:justify-between md:items-center border-t border-gray-200 dark:border-gray-700\"),a(n,\"class\",\"bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden dark:bg-slate-900 dark:border-gray-700\"),a(R,\"class\",\"p-1.5 min-w-full inline-block align-middle\"),a(r,\"class\",\"-m-1.5 overflow-x-auto\"),a(t,\"class\",\"flex flex-col\"),a(T,\"class\",\"max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 mx-auto\")},m(Te,Xe){Ve&&Ve.m(Te,Xe),V(Te,e,Xe),V(Te,T,Xe),l(T,t),l(t,r),l(r,R),l(R,n),l(n,s),l(s,S),l(s,A),l(s,o),l(o,i),l(i,_),l(i,c),l(i,P),l(n,p),l(n,C),l(C,L),l(L,I);for(let se=0;se<$e.length;se+=1)$e[se]&&$e[se].m(I,null);l(C,u),l(C,H);for(let se=0;se<ve.length;se+=1)ve[se]&&ve[se].m(H,null);l(n,b),l(n,M),l(M,O),l(O,N),l(O,D),l(O,B),l(B,h),l(h,F),l(h,W),l(h,J),l(O,oe),l(O,z),l(M,Oe),l(M,Ue),l(Ue,ye),l(ye,TE),l(ye,dE),l(ye,me),V(Te,fE,Xe),Fe&&Fe.m(Te,Xe),V(Te,rE,Xe),pE=!0,Tt||(rt=[Ne(_,\"click\",E[11]),Ne(P,\"click\",E[12]),Ne(TE,\"click\",E[9]),Ne(me,\"click\",E[10])],Tt=!0)},p(Te,[Xe]){if(Te[6]?Ve?(Ve.p(Te,Xe),Xe&64&&m(Ve,1)):(Ve=iR(Te),Ve.c(),m(Ve,1),Ve.m(e.parentNode,e)):Ve&&(Ge(),y(Ve,1,1,()=>{Ve=null}),ge()),Xe&256){ME=De(Te[8]);let se;for(se=0;se<ME.length;se+=1){const gE=oR(Te,ME,se);$e[se]?$e[se].p(gE,Xe):($e[se]=aR(gE),$e[se].c(),$e[se].m(I,null))}for(;se<$e.length;se+=1)$e[se].d(1);$e.length=ME.length}if(Xe&304){ze=De(Te[4]);let se;for(se=0;se<ze.length;se+=1){const gE=sR(Te,ze,se);ve[se]?ve[se].p(gE,Xe):(ve[se]=NR(gE),ve[se].c(),ve[se].m(H,null))}for(;se<ve.length;se+=1)ve[se].d(1);ve.length=ze.length}(!pE||Xe&4)&&G!==(G=Te[2]+1+\"\")&&Le(F,G),(!pE||Xe&8)&&x!==(x=Math.min(Te[3],Te[7].length)+\"\")&&Le(J,x),Te[5]!=null?Fe?(Fe.p(Te,Xe),Xe&32&&m(Fe,1)):(Fe=lR(Te),Fe.c(),m(Fe,1),Fe.m(rE.parentNode,rE)):Fe&&(Ge(),y(Fe,1,1,()=>{Fe=null}),ge())},i(Te){pE||(m(Ve),m(Fe),pE=!0)},o(Te){y(Ve),y(Fe),pE=!1},d(Te){Te&&(Y(e),Y(T),Y(fE),Y(rE)),Ve&&Ve.d(Te),nE($e,Te),nE(ve,Te),Fe&&Fe.d(Te),Tt=!1,NE(rt)}}}function Ua(E,e,T){let{df:t}=e,{onTrain:r}=e,{removeTrainingData:R}=e,n=JSON.parse(t),s=n.length>0?Object.keys(n[0]):[],S=10,A=1,o=Math.ceil(n.length/S),i=(A-1)*S,_=A*S,c=n.slice(i,_);const P=()=>{A>1&&T(16,A--,A)},p=()=>{A<o&&T(16,A++,A)},C=()=>{T(16,A=1),T(15,S=n.length)};let L=null,I=!1;const u=()=>{T(6,I=!0)},H=()=>{T(6,I=!1)},b=(N,D)=>{T(5,L=N[D])},M=()=>{T(5,L=null)},O=()=>{L&&R(L)};return E.$$set=N=>{\"df\"in N&&T(14,t=N.df),\"onTrain\"in N&&T(0,r=N.onTrain),\"removeTrainingData\"in N&&T(1,R=N.removeTrainingData)},E.$$.update=()=>{E.$$.dirty&98304&&T(2,i=(A-1)*S),E.$$.dirty&98304&&T(3,_=A*S),E.$$.dirty&12&&T(4,c=n.slice(i,_)),E.$$.dirty&32768&&T(17,o=Math.ceil(n.length/S)),E.$$.dirty&196608&&console.log(A,o)},[r,R,i,_,c,L,I,n,s,P,p,C,u,H,t,S,A,o,b,M,O]}class ma extends ue{constructor(e){super(),Ce(this,e,Ua,Ma,_e,{df:14,onTrain:0,removeTrainingData:1})}}function ha(E){let e;return{c(){e=f(\"div\"),e.innerHTML='<div class=\"flex flex-auto flex-col justify-center items-center p-4 md:p-5\"><div class=\"flex justify-center\"><div class=\"animate-spin inline-block w-6 h-6 border-[3px] border-current border-t-transparent text-blue-600 rounded-full\" role=\"status\" aria-label=\"loading\"><span class=\"sr-only\">Loading...</span></div></div></div>',a(e,\"class\",\"min-h-[15rem] flex flex-col bg-white border shadow-sm rounded-xl dark:bg-gray-800 dark:border-gray-700 dark:shadow-slate-700/[.7]\")},m(T,t){V(T,e,t)},p:j,i:j,o:j,d(T){T&&Y(e)}}}function Ga(E){let e,T,t,r;const R=[Ha,ga],n=[];function s(S,A){return S[0].type===\"df\"?0:S[0].type===\"error\"?1:-1}return~(e=s(E))&&(T=n[e]=R[e](E)),{c(){T&&T.c(),t=je()},m(S,A){~e&&n[e].m(S,A),V(S,t,A),r=!0},p(S,A){let o=e;e=s(S),e===o?~e&&n[e].p(S,A):(T&&(Ge(),y(n[o],1,1,()=>{n[o]=null}),ge()),~e?(T=n[e],T?T.p(S,A):(T=n[e]=R[e](S),T.c()),m(T,1),T.m(t.parentNode,t)):T=null)},i(S){r||(m(T),r=!0)},o(S){y(T),r=!1},d(S){S&&Y(t),~e&&n[e].d(S)}}}function ga(E){let e,T;return e=new PT({props:{message:E[0].error}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&1&&(R.message=t[0].error),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Ha(E){let e,T;return e=new ma({props:{df:E[0].df,removeTrainingData:Un,onTrain:Hn}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&1&&(R.df=t[0].df),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function ba(E){let e,T,t,r,R;const n=[Ga,ha],s=[];function S(A,o){return A[0]!==null?0:1}return t=S(E),r=s[t]=n[t](E),{c(){e=f(\"div\"),T=f(\"div\"),r.c(),a(T,\"class\",\"py-10 lg:py-14\"),a(e,\"class\",\"relative h-screen w-full lg:pl-64\")},m(A,o){V(A,e,o),l(e,T),s[t].m(T,null),R=!0},p(A,[o]){let i=t;t=S(A),t===i?s[t].p(A,o):(Ge(),y(s[i],1,1,()=>{s[i]=null}),ge(),r=s[t],r?r.p(A,o):(r=s[t]=n[t](A),r.c()),m(r,1),r.m(T,null))},i(A){R||(m(r),R=!0)},o(A){y(r),R=!1},d(A){A&&Y(e),s[t].d()}}}function ya(E,e,T){let t;return eE(E,gt,r=>T(0,t=r)),[t]}class Ba extends ue{constructor(e){super(),Ce(this,e,ya,ba,_e,{})}}function va(E){let e;return{c(){e=f(\"body\"),e.innerHTML=`<div class=\"max-w-[50rem] flex flex-col mx-auto size-full\"><header class=\"mb-auto flex justify-center z-50 w-full py-4\"><nav class=\"px-4 sm:px-6 lg:px-8\" aria-label=\"Global\"><span class=\"flex-none text-xl font-semibold sm:text-3xl dark:text-white\">Vanna.AI</span></nav></header> <div class=\"text-center py-10 px-4 sm:px-6 lg:px-8\"><h1 class=\"block text-7xl font-bold text-gray-800 sm:text-9xl dark:text-white\">No Training Data</h1> <h1 class=\"block text-2xl font-bold text-white\">Did you read the docs?</h1> <p class=\"mt-3 text-gray-600 dark:text-gray-400\">Oops, something went wrong.</p> <p class=\"text-gray-600 dark:text-gray-400\">You need some training data before you can use Vanna</p> <div class=\"mt-5 flex flex-col justify-center items-center gap-2 sm:flex-row sm:gap-3\"><a class=\"w-full sm:w-auto py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-semibold rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none dark:focus:outline-none dark:focus:ring-1 dark:focus:ring-gray-600\" href=\"https://github.com/htmlstreamofficial/preline/tree/main/examples/html\" target=\"_blank\"><svg class=\"flex-shrink-0 size-4\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\"><path d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z\"></path></svg>\n            Get the source code</a> <a class=\"w-full sm:w-auto py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-semibold rounded-lg border border-transparent text-blue-600 hover:text-blue-800 disabled:opacity-50 disabled:pointer-events-none dark:text-blue-500 dark:hover:text-blue-400 dark:focus:outline-none dark:focus:ring-1 dark:focus:ring-gray-600\" href=\"../examples.html\"><svg class=\"flex-shrink-0 size-4\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m15 18-6-6 6-6\"></path></svg>\n            Back to examples</a></div></div> <footer class=\"mt-auto text-center py-5\"><div class=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8\"><p class=\"text-sm text-gray-500\">© All Rights Reserved. 2022.</p></div></footer></div>`,a(e,\"class\",\"flex h-full\")},m(T,t){V(T,e,t)},p:j,i:j,o:j,d(T){T&&Y(e)}}}class Fa extends ue{constructor(e){super(),Ce(this,e,null,va,_e,{})}}function Ya(E){let e,T,t;return{c(){e=f(\"div\"),T=f(\"div\"),t=f(\"div\"),a(t,\"class\",\"mt-7 bg-white border border-gray-200 rounded-xl shadow-sm dark:bg-gray-800 dark:border-gray-700\"),a(T,\"class\",\"w-full max-w-md mx-auto p-6\"),a(e,\"class\",\"dark:bg-slate-900 bg-gray-100 flex h-screen items-center py-16\")},m(r,R){V(r,e,R),l(e,T),l(T,t),t.innerHTML=E[0]},p(r,[R]){R&1&&(t.innerHTML=r[0])},i:j,o:j,d(r){r&&Y(e)}}}function Va(E,e,T){let t;return eE(E,bt,r=>T(0,t=r)),[t]}class Wa extends ue{constructor(e){super(),Ce(this,e,Va,Ya,_e,{})}}function wa(E){let e,T,t,r,R,n;return{c(){e=f(\"div\"),T=f(\"div\"),T.innerHTML='<h3 class=\"text-lg font-bold text-gray-800 dark:text-white\">New</h3> <p class=\"mt-1 text-xs font-medium uppercase text-gray-500 dark:text-neutral-500\">Create a New Function</p>',t=$(),r=f(\"button\"),r.innerHTML=`Create\n    <svg class=\"flex-shrink-0 size-4\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m9 18 6-6-6-6\"></path></svg>`,a(r,\"class\",\"mt-3 inline-flex items-center gap-x-1 text-sm font-semibold rounded-lg border border-transparent text-green-600 hover:text-green-800 disabled:opacity-50 disabled:pointer-events-none dark:text-green-500 dark:hover:text-green-400\"),a(e,\"class\",\"p-4 flex flex-col justify-between bg-white border border-t-4 border-t-green-600 shadow-sm rounded-xl dark:bg-neutral-900 dark:border-neutral-700 dark:border-t-green-500 dark:shadow-neutral-700/70\")},m(s,S){V(s,e,S),l(e,T),l(e,t),l(e,r),R||(n=Ne(r,\"click\",E[0]),R=!0)},p:j,i:j,o:j,d(s){s&&Y(e),R=!1,n()}}}function $a(E){return[()=>{it()}]}class xa extends ue{constructor(e){super(),Ce(this,e,$a,wa,_e,{})}}function _R(E,e,T){const t=E.slice();return t[1]=e[T],t}function LR(E){let e,T;return e=new aE({props:{$$slots:{default:[Xa]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Xa(E){let e;return{c(){e=te(\"No functions found\")},m(T,t){V(T,e,t)},d(T){T&&Y(e)}}}function CR(E){let e,T;return e=new en({props:{functionTemplate:E[1]}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},p(t,r){const R={};r&1&&(R.functionTemplate=t[1]),e.$set(R)},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function ka(E){let e,T,t,r,R,n,s,S,A,o=E[0].length==0&&LR(E),i=De(E[0]),_=[];for(let P=0;P<i.length;P+=1)_[P]=CR(_R(E,i,P));const c=P=>y(_[P],1,1,()=>{_[P]=null});return S=new xa({}),{c(){e=f(\"div\"),T=f(\"div\"),t=f(\"h1\"),t.textContent=\"Functions\",r=$(),o&&o.c(),R=$(),n=f(\"div\");for(let P=0;P<_.length;P+=1)_[P].c();s=$(),K(S.$$.fragment),a(t,\"class\",\"text-3xl font-bold text-gray-800 sm:text-4xl dark:text-white text-center\"),a(n,\"class\",\"grid grid-cols-4 gap-4 p-4\"),a(T,\"class\",\"py-10 lg:py-14\"),a(e,\"class\",\"relative h-screen w-full lg:pl-64\")},m(P,p){V(P,e,p),l(e,T),l(T,t),l(T,r),o&&o.m(T,null),l(T,R),l(T,n);for(let C=0;C<_.length;C+=1)_[C]&&_[C].m(n,null);l(n,s),X(S,n,null),A=!0},p(P,[p]){if(P[0].length==0?o?p&1&&m(o,1):(o=LR(P),o.c(),m(o,1),o.m(T,R)):o&&(Ge(),y(o,1,1,()=>{o=null}),ge()),p&1){i=De(P[0]);let C;for(C=0;C<i.length;C+=1){const L=_R(P,i,C);_[C]?(_[C].p(L,p),m(_[C],1)):(_[C]=CR(L),_[C].c(),m(_[C],1),_[C].m(n,s))}for(Ge(),C=i.length;C<_.length;C+=1)c(C);ge()}},i(P){if(!A){m(o);for(let p=0;p<i.length;p+=1)m(_[p]);m(S.$$.fragment,P),A=!0}},o(P){y(o),_=_.filter(Boolean);for(let p=0;p<_.length;p+=1)y(_[p]);y(S.$$.fragment,P),A=!1},d(P){P&&Y(e),o&&o.d(),nE(_,P),k(S)}}}function Ka(E,e,T){let t;return eE(E,MR,r=>T(0,t=r)),[t]}class Ja extends ue{constructor(e){super(),Ce(this,e,Ka,ka,_e,{})}}function uR(E){let e,T;return e=new Kn({}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function qa(E){let e,T;return e=new aE({props:{$$slots:{default:[EI]},$$scope:{ctx:E}}}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Qa(E){let e,T;return e=new Ja({}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function Za(E){let e,T;return e=new Wa({}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function ja(E){let e,T;return e=new Fa({}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function za(E){let e,T;return e=new Ba({}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function eI(E){let e,T;return e=new la({}),{c(){K(e.$$.fragment)},m(t,r){X(e,t,r),T=!0},i(t){T||(m(e.$$.fragment,t),T=!0)},o(t){y(e.$$.fragment,t),T=!1},d(t){k(e,t)}}}function EI(E){let e;return{c(){e=te(\"Unknown page\")},m(T,t){V(T,e,t)},d(T){T&&Y(e)}}}function tI(E){let e,T,t,r,R,n=E[0]!==\"login\"&&uR();const s=[eI,za,ja,Za,Qa,qa],S=[];function A(o,i){return o[0]===\"chat\"?0:o[0]===\"training-data\"?1:o[0]===\"no-training-data\"?2:o[0]===\"login\"?3:o[0]===\"functions\"?4:5}return t=A(E),r=S[t]=s[t](E),{c(){e=f(\"main\"),n&&n.c(),T=$(),r.c()},m(o,i){V(o,e,i),n&&n.m(e,null),l(e,T),S[t].m(e,null),R=!0},p(o,[i]){o[0]!==\"login\"?n?i&1&&m(n,1):(n=uR(),n.c(),m(n,1),n.m(e,T)):n&&(Ge(),y(n,1,1,()=>{n=null}),ge());let _=t;t=A(o),t!==_&&(Ge(),y(S[_],1,1,()=>{S[_]=null}),ge(),r=S[t],r||(r=S[t]=s[t](o),r.c()),m(r,1),r.m(e,null))},i(o){R||(m(n),m(r),R=!0)},o(o){y(n),y(r),R=!1},d(o){o&&Y(e),n&&n.d(),S[t].d()}}}function TI(E,e,T){let t;return eE(E,DE,r=>T(0,t=r)),DR(async()=>{pn(),mR();const R=new URL(window.location.href).hash.slice(1);R===\"training-data\"?hR():R===\"functions\"?cT():it()}),[t]}class rI extends ue{constructor(e){super(),Ce(this,e,TI,tI,_e,{})}}new rI({target:document.getElementById(\"app\")});\n'''\n"
  },
  {
    "path": "src/vanna/legacy/flask/auth.py",
    "content": "from abc import ABC, abstractmethod\n\nimport flask\n\n\nclass AuthInterface(ABC):\n    @abstractmethod\n    def get_user(self, flask_request) -> any:\n        pass\n\n    @abstractmethod\n    def is_logged_in(self, user: any) -> bool:\n        pass\n\n    @abstractmethod\n    def override_config_for_user(self, user: any, config: dict) -> dict:\n        pass\n\n    @abstractmethod\n    def login_form(self) -> str:\n        pass\n\n    @abstractmethod\n    def login_handler(self, flask_request) -> str:\n        pass\n\n    @abstractmethod\n    def callback_handler(self, flask_request) -> str:\n        pass\n\n    @abstractmethod\n    def logout_handler(self, flask_request) -> str:\n        pass\n\n\nclass NoAuth(AuthInterface):\n    def get_user(self, flask_request) -> any:\n        return {}\n\n    def is_logged_in(self, user: any) -> bool:\n        return True\n\n    def override_config_for_user(self, user: any, config: dict) -> dict:\n        return config\n\n    def login_form(self) -> str:\n        return \"\"\n\n    def login_handler(self, flask_request) -> str:\n        return \"No login required\"\n\n    def callback_handler(self, flask_request) -> str:\n        return \"No login required\"\n\n    def logout_handler(self, flask_request) -> str:\n        return \"No login required\"\n"
  },
  {
    "path": "src/vanna/legacy/google/__init__.py",
    "content": "from .bigquery_vector import BigQuery_VectorStore\nfrom .gemini_chat import GoogleGeminiChat\n"
  },
  {
    "path": "src/vanna/legacy/google/bigquery_vector.py",
    "content": "import datetime\nimport os\nimport uuid\nfrom typing import List, Optional\nfrom vertexai.language_models import TextEmbeddingInput, TextEmbeddingModel\n\nimport pandas as pd\nfrom google.cloud import bigquery\n\nfrom ..base import VannaBase\n\n\nclass BigQuery_VectorStore(VannaBase):\n    def __init__(self, config: dict, **kwargs):\n        self.config = config\n\n        self.n_results_sql = config.get(\"n_results_sql\", config.get(\"n_results\", 10))\n        self.n_results_documentation = config.get(\n            \"n_results_documentation\", config.get(\"n_results\", 10)\n        )\n        self.n_results_ddl = config.get(\"n_results_ddl\", config.get(\"n_results\", 10))\n\n        if \"api_key\" in config or os.getenv(\"GOOGLE_API_KEY\"):\n            \"\"\"\n            If Google api_key is provided through config\n            or set as an environment variable, assign it.\n            \"\"\"\n            print(\"Configuring genai\")\n            self.type = \"GEMINI\"\n            import google.generativeai as genai\n\n            genai.configure(api_key=config[\"api_key\"])\n\n            self.genai = genai\n        else:\n            self.type = \"VERTEX_AI\"\n            # Authenticate using VertexAI\n\n        if self.config.get(\"project_id\"):\n            self.project_id = self.config.get(\"project_id\")\n        else:\n            self.project_id = os.getenv(\"GOOGLE_CLOUD_PROJECT\")\n\n        if self.project_id is None:\n            raise ValueError(\"Project ID is not set\")\n\n        self.conn = bigquery.Client(project=self.project_id)\n\n        dataset_name = self.config.get(\"bigquery_dataset_name\", \"vanna_managed\")\n        self.dataset_id = f\"{self.project_id}.{dataset_name}\"\n        dataset = bigquery.Dataset(self.dataset_id)\n\n        try:\n            self.conn.get_dataset(self.dataset_id)  # Make an API request.\n            print(f\"Dataset {self.dataset_id} already exists\")\n        except Exception:\n            # Dataset does not exist, create it\n            dataset.location = \"US\"\n            self.conn.create_dataset(dataset, timeout=30)  # Make an API request.\n            print(f\"Created dataset {self.dataset_id}\")\n\n        # Create a table called training_data in the dataset that contains the columns:\n        # id, training_data_type, question, content, embedding, created_at\n\n        self.table_id = f\"{self.dataset_id}.training_data\"\n        schema = [\n            bigquery.SchemaField(\"id\", \"STRING\", mode=\"REQUIRED\"),\n            bigquery.SchemaField(\"training_data_type\", \"STRING\", mode=\"REQUIRED\"),\n            bigquery.SchemaField(\"question\", \"STRING\", mode=\"REQUIRED\"),\n            bigquery.SchemaField(\"content\", \"STRING\", mode=\"REQUIRED\"),\n            bigquery.SchemaField(\"embedding\", \"FLOAT64\", mode=\"REPEATED\"),\n            bigquery.SchemaField(\"created_at\", \"TIMESTAMP\", mode=\"REQUIRED\"),\n        ]\n\n        table = bigquery.Table(self.table_id, schema=schema)\n\n        try:\n            self.conn.get_table(self.table_id)  # Make an API request.\n            print(f\"Table {self.table_id} already exists\")\n        except Exception:\n            # Table does not exist, create it\n            self.conn.create_table(table, timeout=30)  # Make an API request.\n            print(f\"Created table {self.table_id}\")\n\n        # Create VECTOR INDEX IF NOT EXISTS\n        # TODO: This requires 5000 rows before it can be created\n        # vector_index_query = f\"\"\"\n        # CREATE VECTOR INDEX IF NOT EXISTS my_index\n        # ON `{self.table_id}`(embedding)\n        # OPTIONS(\n        #     distance_type='COSINE',\n        #     index_type='IVF',\n        #     ivf_options='{{\"num_lists\": 1000}}'\n        # )\n        # \"\"\"\n\n        # try:\n        #     self.conn.query(vector_index_query).result()  # Make an API request.\n        #     print(f\"Vector index on {self.table_id} created or already exists\")\n        # except Exception as e:\n        #     print(f\"Failed to create vector index: {e}\")\n\n    def store_training_data(\n        self,\n        training_data_type: str,\n        question: str,\n        content: str,\n        embedding: List[float],\n        **kwargs,\n    ) -> str:\n        id = str(uuid.uuid4())\n        created_at = datetime.datetime.now()\n        self.conn.insert_rows_json(\n            self.table_id,\n            [\n                {\n                    \"id\": id,\n                    \"training_data_type\": training_data_type,\n                    \"question\": question,\n                    \"content\": content,\n                    \"embedding\": embedding,\n                    \"created_at\": created_at.isoformat(),\n                }\n            ],\n        )\n\n        return id\n\n    def fetch_similar_training_data(\n        self, training_data_type: str, question: str, n_results, **kwargs\n    ) -> pd.DataFrame:\n        question_embedding = self.generate_question_embedding(question)\n\n        query = f\"\"\"\n        SELECT\n            base.id as id,\n            base.question as question,\n            base.training_data_type as training_data_type,\n            base.content as content,\n            distance\n        FROM\n            VECTOR_SEARCH(\n                TABLE `{self.table_id}`,\n                'embedding',\n                (SELECT * FROM UNNEST([STRUCT({question_embedding})])),\n                top_k => 5,\n                distance_type => 'COSINE',\n                options => '{{\"use_brute_force\":true}}'\n            )\n        WHERE\n            base.training_data_type = '{training_data_type}'\n        \"\"\"\n\n        results = self.conn.query(query).result().to_dataframe()\n        return results\n\n    def get_embeddings(self, data: str, task: str) -> List[float]:\n        embeddings = None\n\n        if self.type == \"VERTEX_AI\":\n            input = [TextEmbeddingInput(data, task)]\n            model = TextEmbeddingModel.from_pretrained(\"text-embedding-004\")\n\n            result = model.get_embeddings(input)\n\n            if len(result) > 0:\n                embeddings = result[0].values\n        else:\n            # Use Gemini Consumer API\n            result = self.genai.embed_content(\n                model=\"models/text-embedding-004\", content=data, task_type=task\n            )\n\n            if \"embedding\" in result:\n                embeddings = result[\"embedding\"]\n\n        return embeddings\n\n    def generate_question_embedding(self, data: str, **kwargs) -> List[float]:\n        result = self.get_embeddings(data, \"RETRIEVAL_QUERY\")\n\n        if result is not None:\n            return result\n        else:\n            raise ValueError(\"No embeddings returned\")\n\n    def generate_storage_embedding(self, data: str, **kwargs) -> List[float]:\n        result = self.get_embeddings(data, \"RETRIEVAL_DOCUMENT\")\n\n        if result is not None:\n            return result\n        else:\n            raise ValueError(\"No embeddings returned\")\n\n        # task = \"RETRIEVAL_DOCUMENT\"\n        # inputs = [TextEmbeddingInput(data, task)]\n        # embeddings = self.vertex_embedding_model.get_embeddings(inputs)\n\n        # if len(embeddings) == 0:\n        #     raise ValueError(\"No embeddings returned\")\n\n        # return embeddings[0].values\n\n        return result\n\n    def generate_embedding(self, data: str, **kwargs) -> List[float]:\n        return self.generate_storage_embedding(data, **kwargs)\n\n    def get_similar_question_sql(self, question: str, **kwargs) -> list:\n        df = self.fetch_similar_training_data(\n            training_data_type=\"sql\", question=question, n_results=self.n_results_sql\n        )\n\n        # Return a list of dictionaries with only question, sql fields. The content field needs to be renamed to sql\n        return df.rename(columns={\"content\": \"sql\"})[[\"question\", \"sql\"]].to_dict(\n            orient=\"records\"\n        )\n\n    def get_related_ddl(self, question: str, **kwargs) -> list:\n        df = self.fetch_similar_training_data(\n            training_data_type=\"ddl\", question=question, n_results=self.n_results_ddl\n        )\n\n        # Return a list of strings of the content\n        return df[\"content\"].tolist()\n\n    def get_related_documentation(self, question: str, **kwargs) -> list:\n        df = self.fetch_similar_training_data(\n            training_data_type=\"documentation\",\n            question=question,\n            n_results=self.n_results_documentation,\n        )\n\n        # Return a list of strings of the content\n        return df[\"content\"].tolist()\n\n    def add_question_sql(self, question: str, sql: str, **kwargs) -> str:\n        doc = {\"question\": question, \"sql\": sql}\n\n        embedding = self.generate_embedding(str(doc))\n\n        return self.store_training_data(\n            training_data_type=\"sql\",\n            question=question,\n            content=sql,\n            embedding=embedding,\n        )\n\n    def add_ddl(self, ddl: str, **kwargs) -> str:\n        embedding = self.generate_embedding(ddl)\n\n        return self.store_training_data(\n            training_data_type=\"ddl\", question=\"\", content=ddl, embedding=embedding\n        )\n\n    def add_documentation(self, documentation: str, **kwargs) -> str:\n        embedding = self.generate_embedding(documentation)\n\n        return self.store_training_data(\n            training_data_type=\"documentation\",\n            question=\"\",\n            content=documentation,\n            embedding=embedding,\n        )\n\n    def get_training_data(self, **kwargs) -> pd.DataFrame:\n        query = (\n            f\"SELECT id, training_data_type, question, content FROM `{self.table_id}`\"\n        )\n\n        return self.conn.query(query).result().to_dataframe()\n\n    def remove_training_data(self, id: str, **kwargs) -> bool:\n        query = f\"DELETE FROM `{self.table_id}` WHERE id = '{id}'\"\n\n        try:\n            self.conn.query(query).result()\n            return True\n\n        except Exception as e:\n            print(f\"Failed to remove training data: {e}\")\n            return False\n"
  },
  {
    "path": "src/vanna/legacy/google/gemini_chat.py",
    "content": "import os\n\nfrom ..base import VannaBase\n\n\nclass GoogleGeminiChat(VannaBase):\n    def __init__(self, config=None):\n        VannaBase.__init__(self, config=config)\n\n        # default temperature - can be overrided using config\n        self.temperature = 0.7\n\n        if \"temperature\" in config:\n            self.temperature = config[\"temperature\"]\n\n        if \"model_name\" in config:\n            model_name = config[\"model_name\"]\n        else:\n            model_name = \"gemini-1.5-pro\"\n\n        self.google_api_key = None\n\n        if \"api_key\" in config or os.getenv(\"GOOGLE_API_KEY\"):\n            \"\"\"\n            If Google api_key is provided through config\n            or set as an environment variable, assign it.\n            \"\"\"\n            import google.generativeai as genai\n\n            genai.configure(api_key=config[\"api_key\"])\n            self.chat_model = genai.GenerativeModel(model_name)\n        else:\n            # Authenticate using VertexAI\n            import google.auth\n            import vertexai\n            from vertexai.generative_models import GenerativeModel\n\n            json_file_path = config.get(\n                \"google_credentials\"\n            )  # Assuming the JSON file path is provided in the config\n\n            if not json_file_path or not os.path.exists(json_file_path):\n                raise FileNotFoundError(\n                    f\"JSON credentials file not found at: {json_file_path}\"\n                )\n\n            try:\n                # Validate and set the JSON file path for GOOGLE_APPLICATION_CREDENTIALS\n                os.environ[\"GOOGLE_APPLICATION_CREDENTIALS\"] = json_file_path\n\n                # Initialize VertexAI with the credentials\n                credentials, _ = google.auth.default()\n                vertexai.init(credentials=credentials)\n                self.chat_model = GenerativeModel(model_name)\n            except google.auth.exceptions.DefaultCredentialsError as e:\n                raise RuntimeError(f\"Default credentials error: {e}\")\n            except google.auth.exceptions.TransportError as e:\n                raise RuntimeError(f\"Transport error during authentication: {e}\")\n            except Exception as e:\n                raise RuntimeError(f\"Failed to authenticate using JSON file: {e}\")\n\n    def system_message(self, message: str) -> any:\n        return message\n\n    def user_message(self, message: str) -> any:\n        return message\n\n    def assistant_message(self, message: str) -> any:\n        return message\n\n    def submit_prompt(self, prompt, **kwargs) -> str:\n        response = self.chat_model.generate_content(\n            prompt,\n            generation_config={\n                \"temperature\": self.temperature,\n            },\n        )\n        return response.text\n"
  },
  {
    "path": "src/vanna/legacy/hf/__init__.py",
    "content": "from .hf import Hf\n"
  },
  {
    "path": "src/vanna/legacy/hf/hf.py",
    "content": "import re\nfrom transformers import AutoTokenizer, AutoModelForCausalLM\n\nfrom ..base import VannaBase\n\n\nclass Hf(VannaBase):\n    def __init__(self, config=None):\n        model_name_or_path = self.config.get(\n            \"model_name_or_path\", None\n        )  # e.g. meta-llama/Meta-Llama-3-8B-Instruct or local path to the model checkpoint files\n        # list of quantization methods supported by transformers package: https://huggingface.co/docs/transformers/main/en/quantization/overview\n        quantization_config = self.config.get(\"quantization_config\", None)\n        self.tokenizer = AutoTokenizer.from_pretrained(model_name_or_path)\n        self.model = AutoModelForCausalLM.from_pretrained(\n            model_name_or_path,\n            quantization_config=quantization_config,\n            device_map=\"auto\",\n        )\n\n    def system_message(self, message: str) -> any:\n        return {\"role\": \"system\", \"content\": message}\n\n    def user_message(self, message: str) -> any:\n        return {\"role\": \"user\", \"content\": message}\n\n    def assistant_message(self, message: str) -> any:\n        return {\"role\": \"assistant\", \"content\": message}\n\n    def extract_sql_query(self, text):\n        \"\"\"\n        Extracts the first SQL statement after the word 'select', ignoring case,\n        matches until the first semicolon, three backticks, or the end of the string,\n        and removes three backticks if they exist in the extracted string.\n\n        Args:\n        - text (str): The string to search within for an SQL statement.\n\n        Returns:\n        - str: The first SQL statement found, with three backticks removed, or an empty string if no match is found.\n        \"\"\"\n        # Regular expression to find 'select' (ignoring case) and capture until ';', '```', or end of string\n        pattern = re.compile(r\"select.*?(?:;|```|$)\", re.IGNORECASE | re.DOTALL)\n\n        match = pattern.search(text)\n        if match:\n            # Remove three backticks from the matched string if they exist\n            return match.group(0).replace(\"```\", \"\")\n        else:\n            return text\n\n    def generate_sql(self, question: str, **kwargs) -> str:\n        # Use the super generate_sql\n        sql = super().generate_sql(question, **kwargs)\n\n        # Replace \"\\_\" with \"_\"\n        sql = sql.replace(\"\\\\_\", \"_\")\n\n        sql = sql.replace(\"\\\\\", \"\")\n\n        return self.extract_sql_query(sql)\n\n    def submit_prompt(self, prompt, **kwargs) -> str:\n        input_ids = self.tokenizer.apply_chat_template(\n            prompt, add_generation_prompt=True, return_tensors=\"pt\"\n        ).to(self.model.device)\n\n        outputs = self.model.generate(\n            input_ids,\n            max_new_tokens=512,\n            eos_token_id=self.tokenizer.eos_token_id,\n            do_sample=True,\n            temperature=1,\n            top_p=0.9,\n        )\n        response = outputs[0][input_ids.shape[-1] :]\n        response = self.tokenizer.decode(response, skip_special_tokens=True)\n        self.log(response)\n\n        return response\n"
  },
  {
    "path": "src/vanna/legacy/local.py",
    "content": "from .chromadb.chromadb_vector import ChromaDB_VectorStore\nfrom .openai.openai_chat import OpenAI_Chat\n\n\nclass LocalContext_OpenAI(ChromaDB_VectorStore, OpenAI_Chat):\n    def __init__(self, config=None):\n        ChromaDB_VectorStore.__init__(self, config=config)\n        OpenAI_Chat.__init__(self, config=config)\n"
  },
  {
    "path": "src/vanna/legacy/marqo/__init__.py",
    "content": "from .marqo import Marqo_VectorStore\n"
  },
  {
    "path": "src/vanna/legacy/marqo/marqo.py",
    "content": "import uuid\n\nimport marqo\nimport pandas as pd\n\nfrom ..base import VannaBase\n\n\nclass Marqo_VectorStore(VannaBase):\n    def __init__(self, config=None):\n        VannaBase.__init__(self, config=config)\n\n        if config is not None and \"marqo_url\" in config:\n            marqo_url = config[\"marqo_url\"]\n        else:\n            marqo_url = \"http://localhost:8882\"\n\n        if config is not None and \"marqo_model\" in config:\n            marqo_model = config[\"marqo_model\"]\n        else:\n            marqo_model = \"hf/all_datasets_v4_MiniLM-L6\"\n\n        self.mq = marqo.Client(url=marqo_url)\n\n        for index in [\"vanna-sql\", \"vanna-ddl\", \"vanna-doc\"]:\n            try:\n                self.mq.create_index(index, model=marqo_model)\n            except Exception as e:\n                print(e)\n                print(f\"Marqo index {index} already exists\")\n                pass\n\n    def generate_embedding(self, data: str, **kwargs) -> list[float]:\n        # Marqo doesn't need to generate embeddings\n        pass\n\n    def add_question_sql(self, question: str, sql: str, **kwargs) -> str:\n        id = str(uuid.uuid4()) + \"-sql\"\n        question_sql_dict = {\n            \"question\": question,\n            \"sql\": sql,\n            \"_id\": id,\n        }\n\n        self.mq.index(\"vanna-sql\").add_documents(\n            [question_sql_dict],\n            tensor_fields=[\"question\", \"sql\"],\n        )\n\n        return id\n\n    def add_ddl(self, ddl: str, **kwargs) -> str:\n        id = str(uuid.uuid4()) + \"-ddl\"\n        ddl_dict = {\n            \"ddl\": ddl,\n            \"_id\": id,\n        }\n\n        self.mq.index(\"vanna-ddl\").add_documents(\n            [ddl_dict],\n            tensor_fields=[\"ddl\"],\n        )\n        return id\n\n    def add_documentation(self, documentation: str, **kwargs) -> str:\n        id = str(uuid.uuid4()) + \"-doc\"\n        doc_dict = {\n            \"doc\": documentation,\n            \"_id\": id,\n        }\n\n        self.mq.index(\"vanna-doc\").add_documents(\n            [doc_dict],\n            tensor_fields=[\"doc\"],\n        )\n        return id\n\n    def get_training_data(self, **kwargs) -> pd.DataFrame:\n        data = []\n\n        for hit in self.mq.index(\"vanna-doc\").search(\"\", limit=1000)[\"hits\"]:\n            data.append(\n                {\n                    \"id\": hit[\"_id\"],\n                    \"training_data_type\": \"documentation\",\n                    \"question\": \"\",\n                    \"content\": hit[\"doc\"],\n                }\n            )\n\n        for hit in self.mq.index(\"vanna-ddl\").search(\"\", limit=1000)[\"hits\"]:\n            data.append(\n                {\n                    \"id\": hit[\"_id\"],\n                    \"training_data_type\": \"ddl\",\n                    \"question\": \"\",\n                    \"content\": hit[\"ddl\"],\n                }\n            )\n\n        for hit in self.mq.index(\"vanna-sql\").search(\"\", limit=1000)[\"hits\"]:\n            data.append(\n                {\n                    \"id\": hit[\"_id\"],\n                    \"training_data_type\": \"sql\",\n                    \"question\": hit[\"question\"],\n                    \"content\": hit[\"sql\"],\n                }\n            )\n\n        df = pd.DataFrame(data)\n\n        return df\n\n    def remove_training_data(self, id: str, **kwargs) -> bool:\n        if id.endswith(\"-sql\"):\n            self.mq.index(\"vanna-sql\").delete_documents(ids=[id])\n            return True\n        elif id.endswith(\"-ddl\"):\n            self.mq.index(\"vanna-ddl\").delete_documents(ids=[id])\n            return True\n        elif id.endswith(\"-doc\"):\n            self.mq.index(\"vanna-doc\").delete_documents(ids=[id])\n            return True\n        else:\n            return False\n\n    # Static method to extract the documents from the results of a query\n    @staticmethod\n    def _extract_documents(data) -> list:\n        # Check if 'hits' key is in the dictionary and if it's a list\n        if \"hits\" in data and isinstance(data[\"hits\"], list):\n            # Iterate over each item in 'hits'\n\n            if len(data[\"hits\"]) == 0:\n                return []\n\n            # If there is a \"doc\" key, return the value of that key\n            if \"doc\" in data[\"hits\"][0]:\n                return [hit[\"doc\"] for hit in data[\"hits\"]]\n\n            # If there is a \"ddl\" key, return the value of that key\n            if \"ddl\" in data[\"hits\"][0]:\n                return [hit[\"ddl\"] for hit in data[\"hits\"]]\n\n            # Otherwise return the entire hit\n            return [\n                {key: value for key, value in hit.items() if not key.startswith(\"_\")}\n                for hit in data[\"hits\"]\n            ]\n        else:\n            # Return an empty list if 'hits' is not found or not a list\n            return []\n\n    def get_similar_question_sql(self, question: str, **kwargs) -> list:\n        return Marqo_VectorStore._extract_documents(\n            self.mq.index(\"vanna-sql\").search(question)\n        )\n\n    def get_related_ddl(self, question: str, **kwargs) -> list:\n        return Marqo_VectorStore._extract_documents(\n            self.mq.index(\"vanna-ddl\").search(question)\n        )\n\n    def get_related_documentation(self, question: str, **kwargs) -> list:\n        return Marqo_VectorStore._extract_documents(\n            self.mq.index(\"vanna-doc\").search(question)\n        )\n"
  },
  {
    "path": "src/vanna/legacy/milvus/__init__.py",
    "content": "from .milvus_vector import Milvus_VectorStore\n"
  },
  {
    "path": "src/vanna/legacy/milvus/milvus_vector.py",
    "content": "import uuid\nfrom typing import List\n\nimport pandas as pd\nfrom pymilvus import DataType, MilvusClient, model\n\nfrom ..base import VannaBase\n\n# Setting the URI as a local file, e.g.`./milvus.db`,\n# is the most convenient method, as it automatically utilizes Milvus Lite\n# to store all data in this file.\n#\n# If you have large scale of data such as more than a million docs, we\n# recommend setting up a more performant Milvus server on docker or kubernetes.\n# When using this setup, please use the server URI,\n# e.g.`http://localhost:19530`, as your URI.\n\nDEFAULT_MILVUS_URI = \"./milvus.db\"\n# DEFAULT_MILVUS_URI = \"http://localhost:19530\"\n\nMAX_LIMIT_SIZE = 10_000\n\n\nclass Milvus_VectorStore(VannaBase):\n    \"\"\"\n    Vectorstore implementation using Milvus - https://milvus.io/docs/quickstart.md\n\n    Args:\n        - config (dict, optional): Dictionary of `Milvus_VectorStore config` options. Defaults to `None`.\n            - milvus_client: A `pymilvus.MilvusClient` instance.\n            - embedding_function:\n                A `milvus_model.base.BaseEmbeddingFunction` instance. Defaults to `DefaultEmbeddingFunction()`.\n                For more models, please refer to:\n                https://milvus.io/docs/embeddings.md\n    \"\"\"\n\n    def __init__(self, config=None):\n        VannaBase.__init__(self, config=config)\n\n        if \"milvus_client\" in config:\n            self.milvus_client = config[\"milvus_client\"]\n        else:\n            self.milvus_client = MilvusClient(uri=DEFAULT_MILVUS_URI)\n\n        if \"embedding_function\" in config:\n            self.embedding_function = config.get(\"embedding_function\")\n        else:\n            self.embedding_function = model.DefaultEmbeddingFunction()\n        self._embedding_dim = self.embedding_function.encode_documents([\"foo\"])[\n            0\n        ].shape[0]\n        self._create_collections()\n        self.n_results = config.get(\"n_results\", 10)\n\n    def _create_collections(self):\n        self._create_sql_collection(\"vannasql\")\n        self._create_ddl_collection(\"vannaddl\")\n        self._create_doc_collection(\"vannadoc\")\n\n    def generate_embedding(self, data: str, **kwargs) -> List[float]:\n        return self.embedding_function.encode_documents(data).tolist()\n\n    def _create_sql_collection(self, name: str):\n        if not self.milvus_client.has_collection(collection_name=name):\n            vannasql_schema = MilvusClient.create_schema(\n                auto_id=False,\n                enable_dynamic_field=False,\n            )\n            vannasql_schema.add_field(\n                field_name=\"id\",\n                datatype=DataType.VARCHAR,\n                max_length=65535,\n                is_primary=True,\n            )\n            vannasql_schema.add_field(\n                field_name=\"text\", datatype=DataType.VARCHAR, max_length=65535\n            )\n            vannasql_schema.add_field(\n                field_name=\"sql\", datatype=DataType.VARCHAR, max_length=65535\n            )\n            vannasql_schema.add_field(\n                field_name=\"vector\",\n                datatype=DataType.FLOAT_VECTOR,\n                dim=self._embedding_dim,\n            )\n\n            vannasql_index_params = self.milvus_client.prepare_index_params()\n            vannasql_index_params.add_index(\n                field_name=\"vector\",\n                index_name=\"vector\",\n                index_type=\"AUTOINDEX\",\n                metric_type=\"L2\",\n            )\n            self.milvus_client.create_collection(\n                collection_name=name,\n                schema=vannasql_schema,\n                index_params=vannasql_index_params,\n                consistency_level=\"Strong\",\n            )\n\n    def _create_ddl_collection(self, name: str):\n        if not self.milvus_client.has_collection(collection_name=name):\n            vannaddl_schema = MilvusClient.create_schema(\n                auto_id=False,\n                enable_dynamic_field=False,\n            )\n            vannaddl_schema.add_field(\n                field_name=\"id\",\n                datatype=DataType.VARCHAR,\n                max_length=65535,\n                is_primary=True,\n            )\n            vannaddl_schema.add_field(\n                field_name=\"ddl\", datatype=DataType.VARCHAR, max_length=65535\n            )\n            vannaddl_schema.add_field(\n                field_name=\"vector\",\n                datatype=DataType.FLOAT_VECTOR,\n                dim=self._embedding_dim,\n            )\n\n            vannaddl_index_params = self.milvus_client.prepare_index_params()\n            vannaddl_index_params.add_index(\n                field_name=\"vector\",\n                index_name=\"vector\",\n                index_type=\"AUTOINDEX\",\n                metric_type=\"L2\",\n            )\n            self.milvus_client.create_collection(\n                collection_name=name,\n                schema=vannaddl_schema,\n                index_params=vannaddl_index_params,\n                consistency_level=\"Strong\",\n            )\n\n    def _create_doc_collection(self, name: str):\n        if not self.milvus_client.has_collection(collection_name=name):\n            vannadoc_schema = MilvusClient.create_schema(\n                auto_id=False,\n                enable_dynamic_field=False,\n            )\n            vannadoc_schema.add_field(\n                field_name=\"id\",\n                datatype=DataType.VARCHAR,\n                max_length=65535,\n                is_primary=True,\n            )\n            vannadoc_schema.add_field(\n                field_name=\"doc\", datatype=DataType.VARCHAR, max_length=65535\n            )\n            vannadoc_schema.add_field(\n                field_name=\"vector\",\n                datatype=DataType.FLOAT_VECTOR,\n                dim=self._embedding_dim,\n            )\n\n            vannadoc_index_params = self.milvus_client.prepare_index_params()\n            vannadoc_index_params.add_index(\n                field_name=\"vector\",\n                index_name=\"vector\",\n                index_type=\"AUTOINDEX\",\n                metric_type=\"L2\",\n            )\n            self.milvus_client.create_collection(\n                collection_name=name,\n                schema=vannadoc_schema,\n                index_params=vannadoc_index_params,\n                consistency_level=\"Strong\",\n            )\n\n    def add_question_sql(self, question: str, sql: str, **kwargs) -> str:\n        if len(question) == 0 or len(sql) == 0:\n            raise Exception(\"pair of question and sql can not be null\")\n        _id = str(uuid.uuid4()) + \"-sql\"\n        embedding = self.embedding_function.encode_documents([question])[0]\n        self.milvus_client.insert(\n            collection_name=\"vannasql\",\n            data={\"id\": _id, \"text\": question, \"sql\": sql, \"vector\": embedding},\n        )\n        return _id\n\n    def add_ddl(self, ddl: str, **kwargs) -> str:\n        if len(ddl) == 0:\n            raise Exception(\"ddl can not be null\")\n        _id = str(uuid.uuid4()) + \"-ddl\"\n        embedding = self.embedding_function.encode_documents([ddl])[0]\n        self.milvus_client.insert(\n            collection_name=\"vannaddl\",\n            data={\"id\": _id, \"ddl\": ddl, \"vector\": embedding},\n        )\n        return _id\n\n    def add_documentation(self, documentation: str, **kwargs) -> str:\n        if len(documentation) == 0:\n            raise Exception(\"documentation can not be null\")\n        _id = str(uuid.uuid4()) + \"-doc\"\n        embedding = self.embedding_function.encode_documents([documentation])[0]\n        self.milvus_client.insert(\n            collection_name=\"vannadoc\",\n            data={\"id\": _id, \"doc\": documentation, \"vector\": embedding},\n        )\n        return _id\n\n    def get_training_data(self, **kwargs) -> pd.DataFrame:\n        sql_data = self.milvus_client.query(\n            collection_name=\"vannasql\",\n            output_fields=[\"*\"],\n            limit=MAX_LIMIT_SIZE,\n        )\n        df = pd.DataFrame()\n        df_sql = pd.DataFrame(\n            {\n                \"id\": [doc[\"id\"] for doc in sql_data],\n                \"question\": [doc[\"text\"] for doc in sql_data],\n                \"content\": [doc[\"sql\"] for doc in sql_data],\n            }\n        )\n        df = pd.concat([df, df_sql])\n\n        ddl_data = self.milvus_client.query(\n            collection_name=\"vannaddl\",\n            output_fields=[\"*\"],\n            limit=MAX_LIMIT_SIZE,\n        )\n\n        df_ddl = pd.DataFrame(\n            {\n                \"id\": [doc[\"id\"] for doc in ddl_data],\n                \"question\": [None for doc in ddl_data],\n                \"content\": [doc[\"ddl\"] for doc in ddl_data],\n            }\n        )\n        df = pd.concat([df, df_ddl])\n\n        doc_data = self.milvus_client.query(\n            collection_name=\"vannadoc\",\n            output_fields=[\"*\"],\n            limit=MAX_LIMIT_SIZE,\n        )\n\n        df_doc = pd.DataFrame(\n            {\n                \"id\": [doc[\"id\"] for doc in doc_data],\n                \"question\": [None for doc in doc_data],\n                \"content\": [doc[\"doc\"] for doc in doc_data],\n            }\n        )\n        df = pd.concat([df, df_doc])\n        return df\n\n    def get_similar_question_sql(self, question: str, **kwargs) -> list:\n        search_params = {\n            \"metric_type\": \"L2\",\n            \"params\": {\"nprobe\": 128},\n        }\n        embeddings = self.embedding_function.encode_queries([question])\n        res = self.milvus_client.search(\n            collection_name=\"vannasql\",\n            anns_field=\"vector\",\n            data=embeddings,\n            limit=self.n_results,\n            output_fields=[\"text\", \"sql\"],\n            search_params=search_params,\n        )\n        res = res[0]\n\n        list_sql = []\n        for doc in res:\n            dict = {}\n            dict[\"question\"] = doc[\"entity\"][\"text\"]\n            dict[\"sql\"] = doc[\"entity\"][\"sql\"]\n            list_sql.append(dict)\n        return list_sql\n\n    def get_related_ddl(self, question: str, **kwargs) -> list:\n        search_params = {\n            \"metric_type\": \"L2\",\n            \"params\": {\"nprobe\": 128},\n        }\n        embeddings = self.embedding_function.encode_queries([question])\n        res = self.milvus_client.search(\n            collection_name=\"vannaddl\",\n            anns_field=\"vector\",\n            data=embeddings,\n            limit=self.n_results,\n            output_fields=[\"ddl\"],\n            search_params=search_params,\n        )\n        res = res[0]\n\n        list_ddl = []\n        for doc in res:\n            list_ddl.append(doc[\"entity\"][\"ddl\"])\n        return list_ddl\n\n    def get_related_documentation(self, question: str, **kwargs) -> list:\n        search_params = {\n            \"metric_type\": \"L2\",\n            \"params\": {\"nprobe\": 128},\n        }\n        embeddings = self.embedding_function.encode_queries([question])\n        res = self.milvus_client.search(\n            collection_name=\"vannadoc\",\n            anns_field=\"vector\",\n            data=embeddings,\n            limit=self.n_results,\n            output_fields=[\"doc\"],\n            search_params=search_params,\n        )\n        res = res[0]\n\n        list_doc = []\n        for doc in res:\n            list_doc.append(doc[\"entity\"][\"doc\"])\n        return list_doc\n\n    def remove_training_data(self, id: str, **kwargs) -> bool:\n        if id.endswith(\"-sql\"):\n            self.milvus_client.delete(collection_name=\"vannasql\", ids=[id])\n            return True\n        elif id.endswith(\"-ddl\"):\n            self.milvus_client.delete(collection_name=\"vannaddl\", ids=[id])\n            return True\n        elif id.endswith(\"-doc\"):\n            self.milvus_client.delete(collection_name=\"vannadoc\", ids=[id])\n            return True\n        else:\n            return False\n"
  },
  {
    "path": "src/vanna/legacy/mistral/__init__.py",
    "content": "from .mistral import Mistral\n"
  },
  {
    "path": "src/vanna/legacy/mistral/mistral.py",
    "content": "import os\n\nfrom mistralai import Mistral as MistralClient\nfrom mistralai import UserMessage\n\nfrom ..base import VannaBase\n\n\nclass Mistral(VannaBase):\n    def __init__(self, config=None):\n        if config is None:\n            raise ValueError(\n                \"For Mistral, config must be provided with an api_key and model\"\n            )\n\n        if \"api_key\" not in config:\n            raise ValueError(\"config must contain a Mistral api_key\")\n\n        if \"model\" not in config:\n            raise ValueError(\"config must contain a Mistral model\")\n\n        api_key = config[\"api_key\"]\n        model = config[\"model\"]\n        self.client = MistralClient(api_key=api_key)\n        self.model = model\n\n    def system_message(self, message: str) -> any:\n        return {\"role\": \"system\", \"content\": message}\n\n    def user_message(self, message: str) -> any:\n        return {\"role\": \"user\", \"content\": message}\n\n    def assistant_message(self, message: str) -> any:\n        return {\"role\": \"assistant\", \"content\": message}\n\n    def generate_sql(self, question: str, **kwargs) -> str:\n        # Use the super generate_sql\n        sql = super().generate_sql(question, **kwargs)\n\n        # Replace \"\\_\" with \"_\"\n        sql = sql.replace(\"\\\\_\", \"_\")\n\n        return sql\n\n    def submit_prompt(self, prompt, **kwargs) -> str:\n        chat_response = self.client.chat.complete(\n            model=self.model,\n            messages=prompt,\n        )\n\n        return chat_response.choices[0].message.content\n"
  },
  {
    "path": "src/vanna/legacy/mock/__init__.py",
    "content": "from .embedding import MockEmbedding\nfrom .llm import MockLLM\nfrom .vectordb import MockVectorDB\n"
  },
  {
    "path": "src/vanna/legacy/mock/embedding.py",
    "content": "from typing import List\n\nfrom ..base import VannaBase\n\n\nclass MockEmbedding(VannaBase):\n    def __init__(self, config=None):\n        pass\n\n    def generate_embedding(self, data: str, **kwargs) -> List[float]:\n        return [1.0, 2.0, 3.0, 4.0, 5.0]\n"
  },
  {
    "path": "src/vanna/legacy/mock/llm.py",
    "content": "from ..base import VannaBase\n\n\nclass MockLLM(VannaBase):\n    def __init__(self, config=None):\n        pass\n\n    def system_message(self, message: str) -> any:\n        return {\"role\": \"system\", \"content\": message}\n\n    def user_message(self, message: str) -> any:\n        return {\"role\": \"user\", \"content\": message}\n\n    def assistant_message(self, message: str) -> any:\n        return {\"role\": \"assistant\", \"content\": message}\n\n    def submit_prompt(self, prompt, **kwargs) -> str:\n        return \"Mock LLM response\"\n"
  },
  {
    "path": "src/vanna/legacy/mock/vectordb.py",
    "content": "import pandas as pd\n\nfrom ..base import VannaBase\n\n\nclass MockVectorDB(VannaBase):\n    def __init__(self, config=None):\n        pass\n\n    def _get_id(self, value: str, **kwargs) -> str:\n        # Hash the value and return the ID\n        return str(hash(value))\n\n    def add_ddl(self, ddl: str, **kwargs) -> str:\n        return self._get_id(ddl)\n\n    def add_documentation(self, doc: str, **kwargs) -> str:\n        return self._get_id(doc)\n\n    def add_question_sql(self, question: str, sql: str, **kwargs) -> str:\n        return self._get_id(question)\n\n    def get_related_ddl(self, question: str, **kwargs) -> list:\n        return []\n\n    def get_related_documentation(self, question: str, **kwargs) -> list:\n        return []\n\n    def get_similar_question_sql(self, question: str, **kwargs) -> list:\n        return []\n\n    def get_training_data(self, **kwargs) -> pd.DataFrame:\n        return pd.DataFrame(\n            {\n                \"id\": {\n                    0: \"19546-ddl\",\n                    1: \"91597-sql\",\n                    2: \"133976-sql\",\n                    3: \"59851-doc\",\n                    4: \"73046-sql\",\n                },\n                \"training_data_type\": {\n                    0: \"ddl\",\n                    1: \"sql\",\n                    2: \"sql\",\n                    3: \"documentation\",\n                    4: \"sql\",\n                },\n                \"question\": {\n                    0: None,\n                    1: \"What are the top selling genres?\",\n                    2: \"What are the low 7 artists by sales?\",\n                    3: None,\n                    4: \"What is the total sales for each customer?\",\n                },\n                \"content\": {\n                    0: \"CREATE TABLE [Invoice]\\n(\\n    [InvoiceId] INTEGER  NOT NULL,\\n    [CustomerId] INTEGER  NOT NULL,\\n    [InvoiceDate] DATETIME  NOT NULL,\\n    [BillingAddress] NVARCHAR(70),\\n    [BillingCity] NVARCHAR(40),\\n    [BillingState] NVARCHAR(40),\\n    [BillingCountry] NVARCHAR(40),\\n    [BillingPostalCode] NVARCHAR(10),\\n    [Total] NUMERIC(10,2)  NOT NULL,\\n    CONSTRAINT [PK_Invoice] PRIMARY KEY  ([InvoiceId]),\\n    FOREIGN KEY ([CustomerId]) REFERENCES [Customer] ([CustomerId]) \\n\\t\\tON DELETE NO ACTION ON UPDATE NO ACTION\\n)\",\n                    1: \"SELECT g.Name AS Genre, SUM(il.Quantity) AS TotalSales\\nFROM Genre g\\nJOIN Track t ON g.GenreId = t.GenreId\\nJOIN InvoiceLine il ON t.TrackId = il.TrackId\\nGROUP BY g.GenreId, g.Name\\nORDER BY TotalSales DESC;\",\n                    2: \"SELECT a.ArtistId, a.Name, SUM(il.Quantity) AS TotalSales\\nFROM Artist a\\nINNER JOIN Album al ON a.ArtistId = al.ArtistId\\nINNER JOIN Track t ON al.AlbumId = t.AlbumId\\nINNER JOIN InvoiceLine il ON t.TrackId = il.TrackId\\nGROUP BY a.ArtistId, a.Name\\nORDER BY TotalSales ASC\\nLIMIT 7;\",\n                    3: \"This is a SQLite database. For dates rememeber to use SQLite syntax.\",\n                    4: \"SELECT c.CustomerId, c.FirstName, c.LastName, SUM(i.Total) AS TotalSales\\nFROM Customer c\\nJOIN Invoice i ON c.CustomerId = i.CustomerId\\nGROUP BY c.CustomerId, c.FirstName, c.LastName;\",\n                },\n            }\n        )\n\n    def remove_training_data(id: str, **kwargs) -> bool:\n        return True\n"
  },
  {
    "path": "src/vanna/legacy/ollama/__init__.py",
    "content": "from .ollama import Ollama\n"
  },
  {
    "path": "src/vanna/legacy/ollama/ollama.py",
    "content": "import json\nimport re\n\nfrom httpx import Timeout\n\nfrom ..base import VannaBase\nfrom ..exceptions import DependencyError\n\n\nclass Ollama(VannaBase):\n    def __init__(self, config=None):\n        try:\n            ollama = __import__(\"ollama\")\n        except ImportError:\n            raise DependencyError(\n                \"You need to install required dependencies to execute this method, run command:\"\n                \" \\npip install ollama\"\n            )\n\n        if not config:\n            raise ValueError(\"config must contain at least Ollama model\")\n        if \"model\" not in config.keys():\n            raise ValueError(\"config must contain at least Ollama model\")\n        self.host = config.get(\"ollama_host\", \"http://localhost:11434\")\n        self.model = config[\"model\"]\n        if \":\" not in self.model:\n            self.model += \":latest\"\n\n        self.ollama_timeout = config.get(\"ollama_timeout\", 240.0)\n\n        self.ollama_client = ollama.Client(\n            self.host, timeout=Timeout(self.ollama_timeout)\n        )\n        self.keep_alive = config.get(\"keep_alive\", None)\n        self.ollama_options = config.get(\"options\", {})\n        self.num_ctx = self.ollama_options.get(\"num_ctx\", 2048)\n        self.__pull_model_if_ne(self.ollama_client, self.model)\n\n    @staticmethod\n    def __pull_model_if_ne(ollama_client, model):\n        model_response = ollama_client.list()\n        model_lists = [\n            model_element[\"model\"] for model_element in model_response.get(\"models\", [])\n        ]\n        if model not in model_lists:\n            ollama_client.pull(model)\n\n    def system_message(self, message: str) -> any:\n        return {\"role\": \"system\", \"content\": message}\n\n    def user_message(self, message: str) -> any:\n        return {\"role\": \"user\", \"content\": message}\n\n    def assistant_message(self, message: str) -> any:\n        return {\"role\": \"assistant\", \"content\": message}\n\n    def extract_sql(self, llm_response):\n        \"\"\"\n        Extracts the first SQL statement after the word 'select', ignoring case,\n        matches until the first semicolon, three backticks, or the end of the string,\n        and removes three backticks if they exist in the extracted string.\n\n        Args:\n        - llm_response (str): The string to search within for an SQL statement.\n\n        Returns:\n        - str: The first SQL statement found, with three backticks removed, or an empty string if no match is found.\n        \"\"\"\n        # Remove ollama-generated extra characters\n        llm_response = llm_response.replace(\"\\\\_\", \"_\")\n        llm_response = llm_response.replace(\"\\\\\", \"\")\n\n        # Regular expression to find ```sql' and capture until '```'\n        sql = re.search(r\"```sql\\n((.|\\n)*?)(?=;|\\[|```)\", llm_response, re.DOTALL)\n        # Regular expression to find 'select, with (ignoring case) and capture until ';', [ (this happens in case of mistral) or end of string\n        select_with = re.search(\n            r\"(select|(with.*?as \\())(.*?)(?=;|\\[|```)\",\n            llm_response,\n            re.IGNORECASE | re.DOTALL,\n        )\n        if sql:\n            self.log(f\"Output from LLM: {llm_response} \\nExtracted SQL: {sql.group(1)}\")\n            return sql.group(1).replace(\"```\", \"\")\n        elif select_with:\n            self.log(\n                f\"Output from LLM: {llm_response} \\nExtracted SQL: {select_with.group(0)}\"\n            )\n            return select_with.group(0)\n        else:\n            return llm_response\n\n    def submit_prompt(self, prompt, **kwargs) -> str:\n        self.log(\n            f\"Ollama parameters:\\n\"\n            f\"model={self.model},\\n\"\n            f\"options={self.ollama_options},\\n\"\n            f\"keep_alive={self.keep_alive}\"\n        )\n        self.log(f\"Prompt Content:\\n{json.dumps(prompt, ensure_ascii=False)}\")\n        response_dict = self.ollama_client.chat(\n            model=self.model,\n            messages=prompt,\n            stream=False,\n            options=self.ollama_options,\n            keep_alive=self.keep_alive,\n        )\n\n        self.log(f\"Ollama Response:\\n{str(response_dict)}\")\n\n        return response_dict[\"message\"][\"content\"]\n"
  },
  {
    "path": "src/vanna/legacy/openai/__init__.py",
    "content": "from .openai_chat import OpenAI_Chat\nfrom .openai_embeddings import OpenAI_Embeddings\n"
  },
  {
    "path": "src/vanna/legacy/openai/openai_chat.py",
    "content": "import os\n\nfrom openai import OpenAI\n\nfrom ..base import VannaBase\n\n\nclass OpenAI_Chat(VannaBase):\n    def __init__(self, client=None, config=None):\n        VannaBase.__init__(self, config=config)\n\n        # default parameters - can be overrided using config\n        self.temperature = 0.7\n\n        if \"temperature\" in config:\n            self.temperature = config[\"temperature\"]\n\n        if \"api_type\" in config:\n            raise Exception(\n                \"Passing api_type is now deprecated. Please pass an OpenAI client instead.\"\n            )\n\n        if \"api_base\" in config:\n            raise Exception(\n                \"Passing api_base is now deprecated. Please pass an OpenAI client instead.\"\n            )\n\n        if \"api_version\" in config:\n            raise Exception(\n                \"Passing api_version is now deprecated. Please pass an OpenAI client instead.\"\n            )\n\n        if client is not None:\n            self.client = client\n            return\n\n        if config is None and client is None:\n            self.client = OpenAI(api_key=os.getenv(\"OPENAI_API_KEY\"))\n            return\n\n        if \"api_key\" in config:\n            self.client = OpenAI(api_key=config[\"api_key\"])\n\n    def system_message(self, message: str) -> any:\n        return {\"role\": \"system\", \"content\": message}\n\n    def user_message(self, message: str) -> any:\n        return {\"role\": \"user\", \"content\": message}\n\n    def assistant_message(self, message: str) -> any:\n        return {\"role\": \"assistant\", \"content\": message}\n\n    def submit_prompt(self, prompt, **kwargs) -> str:\n        if prompt is None:\n            raise Exception(\"Prompt is None\")\n\n        if len(prompt) == 0:\n            raise Exception(\"Prompt is empty\")\n\n        # Count the number of tokens in the message log\n        # Use 4 as an approximation for the number of characters per token\n        num_tokens = 0\n        for message in prompt:\n            num_tokens += len(message[\"content\"]) / 4\n\n        if kwargs.get(\"model\", None) is not None:\n            model = kwargs.get(\"model\", None)\n            print(f\"Using model {model} for {num_tokens} tokens (approx)\")\n            response = self.client.chat.completions.create(\n                model=model,\n                messages=prompt,\n                stop=None,\n                temperature=self.temperature,\n            )\n        elif kwargs.get(\"engine\", None) is not None:\n            engine = kwargs.get(\"engine\", None)\n            print(f\"Using model {engine} for {num_tokens} tokens (approx)\")\n            response = self.client.chat.completions.create(\n                engine=engine,\n                messages=prompt,\n                stop=None,\n                temperature=self.temperature,\n            )\n        elif self.config is not None and \"engine\" in self.config:\n            print(\n                f\"Using engine {self.config['engine']} for {num_tokens} tokens (approx)\"\n            )\n            response = self.client.chat.completions.create(\n                engine=self.config[\"engine\"],\n                messages=prompt,\n                stop=None,\n                temperature=self.temperature,\n            )\n        elif self.config is not None and \"model\" in self.config:\n            print(\n                f\"Using model {self.config['model']} for {num_tokens} tokens (approx)\"\n            )\n            response = self.client.chat.completions.create(\n                model=self.config[\"model\"],\n                messages=prompt,\n                stop=None,\n                temperature=self.temperature,\n            )\n        else:\n            if num_tokens > 3500:\n                model = \"gpt-3.5-turbo-16k\"\n            else:\n                model = \"gpt-3.5-turbo\"\n\n            print(f\"Using model {model} for {num_tokens} tokens (approx)\")\n            response = self.client.chat.completions.create(\n                model=model,\n                messages=prompt,\n                stop=None,\n                temperature=self.temperature,\n            )\n\n        # Find the first response from the chatbot that has text in it (some responses may not have text)\n        for choice in response.choices:\n            if \"text\" in choice:\n                return choice.text\n\n        # If no response with text is found, return the first response's content (which may be empty)\n        return response.choices[0].message.content\n"
  },
  {
    "path": "src/vanna/legacy/openai/openai_embeddings.py",
    "content": "from openai import OpenAI\n\nfrom ..base import VannaBase\n\n\nclass OpenAI_Embeddings(VannaBase):\n    def __init__(self, client=None, config=None):\n        VannaBase.__init__(self, config=config)\n\n        if client is not None:\n            self.client = client\n            return\n\n        if self.client is not None:\n            return\n\n        self.client = OpenAI()\n\n        if config is None:\n            return\n\n        if \"api_type\" in config:\n            self.client.api_type = config[\"api_type\"]\n\n        if \"api_base\" in config:\n            self.client.api_base = config[\"api_base\"]\n\n        if \"api_version\" in config:\n            self.client.api_version = config[\"api_version\"]\n\n        if \"api_key\" in config:\n            self.client.api_key = config[\"api_key\"]\n\n    def generate_embedding(self, data: str, **kwargs) -> list[float]:\n        if self.config is not None and \"engine\" in self.config:\n            embedding = self.client.embeddings.create(\n                engine=self.config[\"engine\"],\n                input=data,\n            )\n        else:\n            embedding = self.client.embeddings.create(\n                model=\"text-embedding-ada-002\",\n                input=data,\n            )\n\n        return embedding.get(\"data\")[0][\"embedding\"]\n"
  },
  {
    "path": "src/vanna/legacy/opensearch/__init__.py",
    "content": "from .opensearch_vector import OpenSearch_VectorStore\nfrom .opensearch_vector_semantic import OpenSearch_Semantic_VectorStore\n"
  },
  {
    "path": "src/vanna/legacy/opensearch/opensearch_vector.py",
    "content": "import base64\nimport uuid\nfrom typing import List\n\nimport pandas as pd\nfrom opensearchpy import OpenSearch\n\nfrom ..base import VannaBase\n\n\nclass OpenSearch_VectorStore(VannaBase):\n    def __init__(self, config=None):\n        VannaBase.__init__(self, config=config)\n        document_index = \"vanna_document_index\"\n        ddl_index = \"vanna_ddl_index\"\n        question_sql_index = \"vanna_questions_sql_index\"\n        if config is not None and \"es_document_index\" in config:\n            document_index = config[\"es_document_index\"]\n        if config is not None and \"es_ddl_index\" in config:\n            ddl_index = config[\"es_ddl_index\"]\n        if config is not None and \"es_question_sql_index\" in config:\n            question_sql_index = config[\"es_question_sql_index\"]\n\n        self.document_index = document_index\n        self.ddl_index = ddl_index\n        self.question_sql_index = question_sql_index\n        print(\n            \"OpenSearch_VectorStore initialized with document_index: \",\n            document_index,\n            \" ddl_index: \",\n            ddl_index,\n            \" question_sql_index: \",\n            question_sql_index,\n        )\n\n        document_index_settings = {\n            \"settings\": {\"index\": {\"number_of_shards\": 6, \"number_of_replicas\": 2}},\n            \"mappings\": {\n                \"properties\": {\n                    \"question\": {\n                        \"type\": \"text\",\n                    },\n                    \"doc\": {\n                        \"type\": \"text\",\n                    },\n                }\n            },\n        }\n\n        ddl_index_settings = {\n            \"settings\": {\"index\": {\"number_of_shards\": 6, \"number_of_replicas\": 2}},\n            \"mappings\": {\n                \"properties\": {\n                    \"ddl\": {\n                        \"type\": \"text\",\n                    },\n                    \"doc\": {\n                        \"type\": \"text\",\n                    },\n                }\n            },\n        }\n\n        question_sql_index_settings = {\n            \"settings\": {\"index\": {\"number_of_shards\": 6, \"number_of_replicas\": 2}},\n            \"mappings\": {\n                \"properties\": {\n                    \"question\": {\n                        \"type\": \"text\",\n                    },\n                    \"sql\": {\n                        \"type\": \"text\",\n                    },\n                }\n            },\n        }\n\n        if config is not None and \"es_document_index_settings\" in config:\n            document_index_settings = config[\"es_document_index_settings\"]\n        if config is not None and \"es_ddl_index_settings\" in config:\n            ddl_index_settings = config[\"es_ddl_index_settings\"]\n        if config is not None and \"es_question_sql_index_settings\" in config:\n            question_sql_index_settings = config[\"es_question_sql_index_settings\"]\n\n        self.document_index_settings = document_index_settings\n        self.ddl_index_settings = ddl_index_settings\n        self.question_sql_index_settings = question_sql_index_settings\n\n        es_urls = None\n        if config is not None and \"es_urls\" in config:\n            es_urls = config[\"es_urls\"]\n\n        # Host and port\n        if config is not None and \"es_host\" in config:\n            host = config[\"es_host\"]\n        else:\n            host = \"localhost\"\n\n        if config is not None and \"es_port\" in config:\n            port = config[\"es_port\"]\n        else:\n            port = 9200\n\n        if config is not None and \"es_ssl\" in config:\n            ssl = config[\"es_ssl\"]\n        else:\n            ssl = False\n\n        if config is not None and \"es_verify_certs\" in config:\n            verify_certs = config[\"es_verify_certs\"]\n        else:\n            verify_certs = False\n\n        # Authentication\n        if config is not None and \"es_user\" in config:\n            auth = (config[\"es_user\"], config[\"es_password\"])\n        else:\n            # Default to admin:admin\n            auth = None\n\n        headers = None\n        # base64 authentication\n        if (\n            config is not None\n            and \"es_encoded_base64\" in config\n            and \"es_user\" in config\n            and \"es_password\" in config\n        ):\n            if config[\"es_encoded_base64\"]:\n                encoded_credentials = base64.b64encode(\n                    (config[\"es_user\"] + \":\" + config[\"es_password\"]).encode(\"utf-8\")\n                ).decode(\"utf-8\")\n                headers = {\"Authorization\": \"Basic \" + encoded_credentials}\n                # remove auth from config\n                auth = None\n\n        # custom headers\n        if config is not None and \"es_headers\" in config:\n            headers = config[\"es_headers\"]\n\n        if config is not None and \"es_timeout\" in config:\n            timeout = config[\"es_timeout\"]\n        else:\n            timeout = 60\n\n        if config is not None and \"es_max_retries\" in config:\n            max_retries = config[\"es_max_retries\"]\n        else:\n            max_retries = 10\n\n        if config is not None and \"es_http_compress\" in config:\n            es_http_compress = config[\"es_http_compress\"]\n        else:\n            es_http_compress = False\n\n        print(\n            \"OpenSearch_VectorStore initialized with es_urls: \",\n            es_urls,\n            \" host: \",\n            host,\n            \" port: \",\n            port,\n            \" ssl: \",\n            ssl,\n            \" verify_certs: \",\n            verify_certs,\n            \" timeout: \",\n            timeout,\n            \" max_retries: \",\n            max_retries,\n        )\n        if es_urls is not None:\n            # Initialize the OpenSearch client by passing a list of URLs\n            self.client = OpenSearch(\n                hosts=[es_urls],\n                http_compress=es_http_compress,\n                use_ssl=ssl,\n                verify_certs=verify_certs,\n                timeout=timeout,\n                max_retries=max_retries,\n                retry_on_timeout=True,\n                http_auth=auth,\n                headers=headers,\n            )\n        else:\n            # Initialize the OpenSearch client by passing a host and port\n            self.client = OpenSearch(\n                hosts=[{\"host\": host, \"port\": port}],\n                http_compress=es_http_compress,\n                use_ssl=ssl,\n                verify_certs=verify_certs,\n                timeout=timeout,\n                max_retries=max_retries,\n                retry_on_timeout=True,\n                http_auth=auth,\n                headers=headers,\n            )\n\n            print(\"OpenSearch_VectorStore initialized with client over \")\n\n        # 执行一个简单的查询来检查连接\n        try:\n            print(\"Connected to OpenSearch cluster:\")\n            info = self.client.info()\n            print(\"OpenSearch cluster info:\", info)\n        except Exception as e:\n            print(\"Error connecting to OpenSearch cluster:\", e)\n\n        # Create the indices if they don't exist\n        self.create_index_if_not_exists(\n            self.document_index, self.document_index_settings\n        )\n        self.create_index_if_not_exists(self.ddl_index, self.ddl_index_settings)\n        self.create_index_if_not_exists(\n            self.question_sql_index, self.question_sql_index_settings\n        )\n\n    def create_index(self):\n        for index in [self.document_index, self.ddl_index, self.question_sql_index]:\n            try:\n                self.client.indices.create(index)\n            except Exception as e:\n                print(\"Error creating index: \", e)\n                print(f\"opensearch index {index} already exists\")\n                pass\n\n    def create_index_if_not_exists(self, index_name: str, index_settings: dict) -> bool:\n        try:\n            if not self.client.indices.exists(index_name):\n                print(f\"Index {index_name} does not exist. Creating...\")\n                self.client.indices.create(index=index_name, body=index_settings)\n                return True\n            else:\n                print(f\"Index {index_name} already exists.\")\n                return False\n        except Exception as e:\n            print(f\"Error creating index: {index_name} \", e)\n            return False\n\n    def add_ddl(self, ddl: str, **kwargs) -> str:\n        # Assuming that you have a DDL index in your OpenSearch\n        id = str(uuid.uuid4()) + \"-ddl\"\n        ddl_dict = {\"ddl\": ddl}\n        response = self.client.index(\n            index=self.ddl_index, body=ddl_dict, id=id, **kwargs\n        )\n        return response[\"_id\"]\n\n    def add_documentation(self, doc: str, **kwargs) -> str:\n        # Assuming you have a documentation index in your OpenSearch\n        id = str(uuid.uuid4()) + \"-doc\"\n        doc_dict = {\"doc\": doc}\n        response = self.client.index(\n            index=self.document_index, id=id, body=doc_dict, **kwargs\n        )\n        return response[\"_id\"]\n\n    def add_question_sql(self, question: str, sql: str, **kwargs) -> str:\n        # Assuming you have a Questions and SQL index in your OpenSearch\n        id = str(uuid.uuid4()) + \"-sql\"\n        question_sql_dict = {\"question\": question, \"sql\": sql}\n        response = self.client.index(\n            index=self.question_sql_index, body=question_sql_dict, id=id, **kwargs\n        )\n        return response[\"_id\"]\n\n    def get_related_ddl(self, question: str, **kwargs) -> List[str]:\n        # Assume you have some vector search mechanism associated with your data\n        query = {\"query\": {\"match\": {\"ddl\": question}}}\n        print(query)\n        response = self.client.search(index=self.ddl_index, body=query, **kwargs)\n        return [hit[\"_source\"][\"ddl\"] for hit in response[\"hits\"][\"hits\"]]\n\n    def get_related_documentation(self, question: str, **kwargs) -> List[str]:\n        query = {\"query\": {\"match\": {\"doc\": question}}}\n        print(query)\n        response = self.client.search(index=self.document_index, body=query, **kwargs)\n        return [hit[\"_source\"][\"doc\"] for hit in response[\"hits\"][\"hits\"]]\n\n    def get_similar_question_sql(self, question: str, **kwargs) -> List[str]:\n        query = {\"query\": {\"match\": {\"question\": question}}}\n        print(query)\n        response = self.client.search(\n            index=self.question_sql_index, body=query, **kwargs\n        )\n        return [\n            (hit[\"_source\"][\"question\"], hit[\"_source\"][\"sql\"])\n            for hit in response[\"hits\"][\"hits\"]\n        ]\n\n    def get_training_data(self, **kwargs) -> pd.DataFrame:\n        # This will be a simple example pulling all data from an index\n        # WARNING: Do not use this approach in production for large indices!\n        data = []\n        response = self.client.search(\n            index=self.document_index, body={\"query\": {\"match_all\": {}}}, size=1000\n        )\n        print(response)\n        # records = [hit['_source'] for hit in response['hits']['hits']]\n        for hit in response[\"hits\"][\"hits\"]:\n            data.append(\n                {\n                    \"id\": hit[\"_id\"],\n                    \"training_data_type\": \"documentation\",\n                    \"question\": \"\",\n                    \"content\": hit[\"_source\"][\"doc\"],\n                }\n            )\n\n        response = self.client.search(\n            index=self.question_sql_index, body={\"query\": {\"match_all\": {}}}, size=1000\n        )\n        # records = [hit['_source'] for hit in response['hits']['hits']]\n        for hit in response[\"hits\"][\"hits\"]:\n            data.append(\n                {\n                    \"id\": hit[\"_id\"],\n                    \"training_data_type\": \"sql\",\n                    \"question\": hit.get(\"_source\", {}).get(\"question\", \"\"),\n                    \"content\": hit.get(\"_source\", {}).get(\"sql\", \"\"),\n                }\n            )\n\n        response = self.client.search(\n            index=self.ddl_index, body={\"query\": {\"match_all\": {}}}, size=1000\n        )\n        # records = [hit['_source'] for hit in response['hits']['hits']]\n        for hit in response[\"hits\"][\"hits\"]:\n            data.append(\n                {\n                    \"id\": hit[\"_id\"],\n                    \"training_data_type\": \"ddl\",\n                    \"question\": \"\",\n                    \"content\": hit[\"_source\"][\"ddl\"],\n                }\n            )\n\n        return pd.DataFrame(data)\n\n    def remove_training_data(self, id: str, **kwargs) -> bool:\n        try:\n            if id.endswith(\"-sql\"):\n                self.client.delete(index=self.question_sql_index, id=id)\n                return True\n            elif id.endswith(\"-ddl\"):\n                self.client.delete(index=self.ddl_index, id=id, **kwargs)\n                return True\n            elif id.endswith(\"-doc\"):\n                self.client.delete(index=self.document_index, id=id, **kwargs)\n                return True\n            else:\n                return False\n        except Exception as e:\n            print(\"Error deleting training dataError deleting training data: \", e)\n            return False\n\n    def generate_embedding(self, data: str, **kwargs) -> list[float]:\n        # opensearch doesn't need to generate embeddings\n        pass\n\n\n# OpenSearch_VectorStore.__init__(self, config={'es_urls':\n# \"https://opensearch-node.test.com:9200\", 'es_encoded_base64': True, 'es_user':\n# \"admin\", 'es_password': \"admin\", 'es_verify_certs': True})\n\n\n# OpenSearch_VectorStore.__init__(self, config={'es_host':\n# \"https://opensearch-node.test.com\", 'es_port': 9200, 'es_user': \"admin\",\n# 'es_password': \"admin\", 'es_verify_certs': True})\n"
  },
  {
    "path": "src/vanna/legacy/opensearch/opensearch_vector_semantic.py",
    "content": "import json\n\nimport pandas as pd\nfrom langchain_community.vectorstores import OpenSearchVectorSearch\n\nfrom ..base import VannaBase\nfrom ..utils import deterministic_uuid\n\n\nclass OpenSearch_Semantic_VectorStore(VannaBase):\n    def __init__(self, config=None):\n        VannaBase.__init__(self, config=config)\n        if config is None:\n            config = {}\n\n        if \"embedding_function\" in config:\n            self.embedding_function = config.get(\"embedding_function\")\n        else:\n            from langchain_huggingface import HuggingFaceEmbeddings\n\n            self.embedding_function = HuggingFaceEmbeddings(\n                model_name=\"all-MiniLM-L6-v2\"\n            )\n\n        self.n_results_sql = config.get(\"n_results_sql\", config.get(\"n_results\", 10))\n        self.n_results_documentation = config.get(\n            \"n_results_documentation\", config.get(\"n_results\", 10)\n        )\n        self.n_results_ddl = config.get(\"n_results_ddl\", config.get(\"n_results\", 10))\n\n        self.document_index = config.get(\"es_document_index\", \"vanna_document_index\")\n        self.ddl_index = config.get(\"es_ddl_index\", \"vanna_ddl_index\")\n        self.question_sql_index = config.get(\n            \"es_question_sql_index\", \"vanna_questions_sql_index\"\n        )\n\n        self.log(\n            f\"OpenSearch_Semantic_VectorStore initialized with document_index: {self.document_index}, ddl_index: {self.ddl_index}, question_sql_index: {self.question_sql_index}\"\n        )\n\n        es_urls = config.get(\"es_urls\", \"https://localhost:9200\")\n        ssl = config.get(\"es_ssl\", True)\n        verify_certs = config.get(\"es_verify_certs\", True)\n\n        if \"es_user\" in config:\n            auth = (config[\"es_user\"], config[\"es_password\"])\n        else:\n            auth = None\n\n        headers = config.get(\"es_headers\", None)\n        timeout = config.get(\"es_timeout\", 60)\n        max_retries = config.get(\"es_max_retries\", 10)\n\n        common_args = {\n            \"opensearch_url\": es_urls,\n            \"embedding_function\": self.embedding_function,\n            \"engine\": \"faiss\",\n            \"http_auth\": auth,\n            \"use_ssl\": ssl,\n            \"verify_certs\": verify_certs,\n            \"timeout\": timeout,\n            \"max_retries\": max_retries,\n            \"retry_on_timeout\": True,\n            \"headers\": headers,\n        }\n\n        self.documentation_store = OpenSearchVectorSearch(\n            index_name=self.document_index, **common_args\n        )\n        self.ddl_store = OpenSearchVectorSearch(\n            index_name=self.ddl_index, **common_args\n        )\n        self.sql_store = OpenSearchVectorSearch(\n            index_name=self.question_sql_index, **common_args\n        )\n\n    def add_ddl(self, ddl: str, **kwargs) -> str:\n        _id = deterministic_uuid(ddl) + \"-ddl\"\n        self.ddl_store.add_texts(texts=[ddl], ids=[_id], **kwargs)\n        return _id\n\n    def add_documentation(self, documentation: str, **kwargs) -> str:\n        _id = deterministic_uuid(documentation) + \"-doc\"\n        self.documentation_store.add_texts(texts=[documentation], ids=[_id], **kwargs)\n        return _id\n\n    def add_question_sql(self, question: str, sql: str, **kwargs) -> str:\n        question_sql_json = json.dumps(\n            {\n                \"question\": question,\n                \"sql\": sql,\n            },\n            ensure_ascii=False,\n        )\n\n        _id = deterministic_uuid(question_sql_json) + \"-sql\"\n        self.sql_store.add_texts(texts=[question_sql_json], ids=[_id], **kwargs)\n        return _id\n\n    def get_related_ddl(self, question: str, **kwargs) -> list:\n        documents = self.ddl_store.similarity_search(\n            query=question, k=self.n_results_ddl\n        )\n        return [document.page_content for document in documents]\n\n    def get_related_documentation(self, question: str, **kwargs) -> list:\n        documents = self.documentation_store.similarity_search(\n            query=question, k=self.n_results_documentation\n        )\n        return [document.page_content for document in documents]\n\n    def get_similar_question_sql(self, question: str, **kwargs) -> list:\n        documents = self.sql_store.similarity_search(\n            query=question, k=self.n_results_sql\n        )\n        return [json.loads(document.page_content) for document in documents]\n\n    def get_training_data(self, **kwargs) -> pd.DataFrame:\n        data = []\n        query = {\"query\": {\"match_all\": {}}}\n\n        indices = [\n            {\"index\": self.document_index, \"type\": \"documentation\"},\n            {\"index\": self.question_sql_index, \"type\": \"sql\"},\n            {\"index\": self.ddl_index, \"type\": \"ddl\"},\n        ]\n\n        # Use documentation_store.client consistently for search on all indices\n        opensearch_client = self.documentation_store.client\n\n        for index_info in indices:\n            index_name = index_info[\"index\"]\n            training_data_type = index_info[\"type\"]\n            scroll = \"1m\"  # keep scroll context for 1 minute\n            response = opensearch_client.search(\n                index=index_name,\n                ignore_unavailable=True,\n                body=query,\n                scroll=scroll,\n                size=1000,\n            )\n\n            scroll_id = response.get(\"_scroll_id\")\n\n            while scroll_id:\n                hits = response[\"hits\"][\"hits\"]\n                if not hits:\n                    break  # No more hits, exit loop\n\n                for hit in hits:\n                    source = hit[\"_source\"]\n                    if training_data_type == \"sql\":\n                        try:\n                            doc_dict = json.loads(source[\"text\"])\n                            content = doc_dict.get(\"sql\")\n                            question = doc_dict.get(\"question\")\n                        except json.JSONDecodeError as e:\n                            self.log(\n                                f\"Skipping row with custom_id {hit['_id']} due to JSON parsing error: {e}\",\n                                \"Error\",\n                            )\n                            continue\n                    else:  # documentation or ddl\n                        content = source[\"text\"]\n                        question = None\n\n                    data.append(\n                        {\n                            \"id\": hit[\"_id\"],\n                            \"training_data_type\": training_data_type,\n                            \"question\": question,\n                            \"content\": content,\n                        }\n                    )\n\n                # Get next batch of results, using documentation_store.client.scroll\n                response = opensearch_client.scroll(scroll_id=scroll_id, scroll=scroll)\n                scroll_id = response.get(\"_scroll_id\")\n\n        return pd.DataFrame(data)\n\n    def remove_training_data(self, id: str, **kwargs) -> bool:\n        try:\n            if id.endswith(\"-sql\"):\n                return self.sql_store.delete(ids=[id], **kwargs)\n            elif id.endswith(\"-ddl\"):\n                return self.ddl_store.delete(ids=[id], **kwargs)\n            elif id.endswith(\"-doc\"):\n                return self.documentation_store.delete(ids=[id], **kwargs)\n            else:\n                return False\n        except Exception as e:\n            self.log(\n                f\"Error deleting training dataError deleting training data: {e}\",\n                \"Error\",\n            )\n            return False\n\n    def generate_embedding(self, data: str, **kwargs) -> list[float]:\n        pass\n"
  },
  {
    "path": "src/vanna/legacy/oracle/__init__.py",
    "content": "from .oracle_vector import Oracle_VectorStore\n"
  },
  {
    "path": "src/vanna/legacy/oracle/oracle_vector.py",
    "content": "import json\nimport uuid\nfrom typing import List, Optional, Tuple\n\nimport oracledb\nimport pandas as pd\nfrom chromadb.utils import embedding_functions\n\nfrom ..base import VannaBase\n\ndefault_ef = embedding_functions.DefaultEmbeddingFunction()\n\n\nclass Oracle_VectorStore(VannaBase):\n    def __init__(self, config=None):\n        VannaBase.__init__(self, config=config)\n\n        if config is not None:\n            self.embedding_function = config.get(\"embedding_function\", default_ef)\n            self.pre_delete_collection = config.get(\"pre_delete_collection\", False)\n            self.cmetadata = config.get(\"cmetadata\", {\"created_by\": \"oracle\"})\n        else:\n            self.embedding_function = default_ef\n            self.pre_delete_collection = False\n            self.cmetadata = {\"created_by\": \"oracle\"}\n\n        self.oracle_conn = oracledb.connect(dsn=config.get(\"dsn\"))\n        self.oracle_conn.call_timeout = 30000\n        self.documentation_collection = \"documentation\"\n        self.ddl_collection = \"ddl\"\n        self.sql_collection = \"sql\"\n        self.n_results = config.get(\"n_results\", 10)\n        self.n_results_ddl = config.get(\"n_results_ddl\", self.n_results)\n        self.n_results_sql = config.get(\"n_results_sql\", self.n_results)\n        self.n_results_documentation = config.get(\n            \"n_results_documentation\", self.n_results\n        )\n        self.create_tables_if_not_exists()\n        self.create_collections_if_not_exists(self.documentation_collection)\n        self.create_collections_if_not_exists(self.ddl_collection)\n        self.create_collections_if_not_exists(self.sql_collection)\n\n    def generate_embedding(self, data: str, **kwargs) -> List[float]:\n        embeddings = self.embedding_function([data])\n        if len(embeddings) == 1:\n            return list(embeddings[0].astype(float))\n        return list(embeddings.astype(float))\n\n    def add_question_sql(self, question: str, sql: str, **kwargs) -> str:\n        cmetadata = self.cmetadata.copy()\n        collection = self.get_collection(self.sql_collection)\n        question_sql_json = json.dumps(\n            {\n                \"question\": question,\n                \"sql\": sql,\n            },\n            ensure_ascii=False,\n        )\n        id = str(uuid.uuid4())\n        embeddings = self.generate_embedding(question)\n        custom_id = id + \"-sql\"\n\n        cursor = self.oracle_conn.cursor()\n        cursor.setinputsizes(None, oracledb.DB_TYPE_VECTOR)\n        cursor.execute(\n            \"\"\"\n          INSERT INTO oracle_embedding (\n              collection_id,\n              embedding,\n              document,\n              cmetadata,\n              custom_id,\n              uuid\n          ) VALUES (\n              :1,\n              TO_VECTOR(:2),\n              :3,\n              :4,\n              :5,\n              :6\n          )\n      \"\"\",\n            [\n                collection[\"uuid\"],\n                embeddings,\n                question_sql_json,\n                json.dumps(cmetadata),\n                custom_id,\n                id,\n            ],\n        )\n\n        self.oracle_conn.commit()\n        cursor.close()\n        return id\n\n    def add_ddl(self, ddl: str, **kwargs) -> str:\n        collection = self.get_collection(self.ddl_collection)\n        question_ddl_json = json.dumps(\n            {\n                \"question\": None,\n                \"ddl\": ddl,\n            },\n            ensure_ascii=False,\n        )\n        id = str(uuid.uuid4())\n        custom_id = id + \"-ddl\"\n        cursor = self.oracle_conn.cursor()\n        cursor.setinputsizes(None, oracledb.DB_TYPE_VECTOR)\n        cursor.execute(\n            \"\"\"\n      INSERT INTO oracle_embedding (\n          collection_id,\n          embedding,\n          document,\n          cmetadata,\n          custom_id,\n          uuid\n      ) VALUES (\n          :1,\n          TO_VECTOR(:2),\n          :3,\n          :4,\n          :5,\n          :6\n      )\n      \"\"\",\n            [\n                collection[\"uuid\"],\n                self.generate_embedding(ddl),\n                question_ddl_json,\n                json.dumps(self.cmetadata),\n                custom_id,\n                id,\n            ],\n        )\n        self.oracle_conn.commit()\n        cursor.close()\n        return id\n\n    def add_documentation(self, documentation: str, **kwargs) -> str:\n        collection = self.get_collection(self.documentation_collection)\n        question_documentation_json = json.dumps(\n            {\n                \"question\": None,\n                \"documentation\": documentation,\n            },\n            ensure_ascii=False,\n        )\n        id = str(uuid.uuid4())\n        custom_id = id + \"-doc\"\n        cursor = self.oracle_conn.cursor()\n        cursor.setinputsizes(None, oracledb.DB_TYPE_VECTOR)\n        cursor.execute(\n            \"\"\"\n      INSERT INTO oracle_embedding (\n          collection_id,\n          embedding,\n          document,\n          cmetadata,\n          custom_id,\n          uuid\n      ) VALUES (\n          :1,\n          TO_VECTOR(:2),\n          :3,\n          :4,\n          :5,\n          :6\n      )\n      \"\"\",\n            [\n                collection[\"uuid\"],\n                self.generate_embedding(documentation),\n                question_documentation_json,\n                json.dumps(self.cmetadata),\n                custom_id,\n                id,\n            ],\n        )\n        self.oracle_conn.commit()\n        cursor.close()\n        return id\n\n    def get_training_data(self, **kwargs) -> pd.DataFrame:\n        df = pd.DataFrame()\n\n        cursor = self.oracle_conn.cursor()\n        sql_collection = self.get_collection(self.sql_collection)\n        cursor.execute(\n            \"\"\"\n                    SELECT\n                        document,\n                        uuid\n                    FROM oracle_embedding\n                    WHERE\n                        collection_id = :1\n                  \"\"\",\n            [sql_collection[\"uuid\"]],\n        )\n        sql_data = cursor.fetchall()\n\n        if sql_data is not None:\n            # Extract the documents and ids\n            documents = [row_data[0] for row_data in sql_data]\n            ids = [row_data[1] for row_data in sql_data]\n\n            # Create a DataFrame\n            df_sql = pd.DataFrame(\n                {\n                    \"id\": ids,\n                    \"question\": [\n                        json.loads(doc)[\"question\"]\n                        if isinstance(doc, str)\n                        else doc[\"question\"]\n                        for doc in documents\n                    ],\n                    \"content\": [\n                        json.loads(doc)[\"sql\"] if isinstance(doc, str) else doc[\"sql\"]\n                        for doc in documents\n                    ],\n                }\n            )\n            df_sql[\"training_data_type\"] = \"sql\"\n            df = pd.concat([df, df_sql])\n\n        ddl_collection = self.get_collection(self.ddl_collection)\n        cursor.execute(\n            \"\"\"\n          SELECT\n              document,\n              uuid\n          FROM oracle_embedding\n          WHERE\n              collection_id = :1\n      \"\"\",\n            [ddl_collection[\"uuid\"]],\n        )\n        ddl_data = cursor.fetchall()\n\n        if ddl_data is not None:\n            # Extract the documents and ids\n            documents = [row_data[0] for row_data in ddl_data]\n            ids = [row_data[1] for row_data in ddl_data]\n\n            # Create a DataFrame\n            df_ddl = pd.DataFrame(\n                {\n                    \"id\": ids,\n                    \"question\": [None for _ in documents],\n                    \"content\": [\n                        json.loads(doc)[\"ddl\"] if isinstance(doc, str) else doc[\"ddl\"]\n                        for doc in documents\n                    ],\n                }\n            )\n            df_ddl[\"training_data_type\"] = \"ddl\"\n            df = pd.concat([df, df_ddl])\n\n        doc_collection = self.get_collection(self.documentation_collection)\n        cursor.execute(\n            \"\"\"\n          SELECT\n              document,\n              uuid\n          FROM oracle_embedding\n          WHERE\n              collection_id = :1\n      \"\"\",\n            [doc_collection[\"uuid\"]],\n        )\n        doc_data = cursor.fetchall()\n\n        if doc_data is not None:\n            # Extract the documents and ids\n            documents = [row_data[0] for row_data in doc_data]\n            ids = [row_data[1] for row_data in doc_data]\n\n            # Create a DataFrame\n            df_doc = pd.DataFrame(\n                {\n                    \"id\": ids,\n                    \"question\": [None for _ in documents],\n                    \"content\": [\n                        json.loads(doc)[\"documentation\"]\n                        if isinstance(doc, str)\n                        else doc[\"documentation\"]\n                        for doc in documents\n                    ],\n                }\n            )\n            df_doc[\"training_data_type\"] = \"documentation\"\n            df = pd.concat([df, df_doc])\n\n        self.oracle_conn.commit()\n        cursor.close()\n        return df\n\n    def remove_training_data(self, id: str, **kwargs) -> bool:\n        cursor = self.oracle_conn.cursor()\n        cursor.execute(\n            \"\"\"\n          DELETE\n          FROM\n              oracle_embedding\n          WHERE\n          uuid  = :1\n      \"\"\",\n            [id],\n        )\n\n        self.oracle_conn.commit()\n        cursor.close()\n        return True\n\n    def update_training_data(\n        self, id: str, train_type: str, question: str, **kwargs\n    ) -> bool:\n        print(f\"{train_type=}\")\n        update_content = kwargs[\"content\"]\n        if train_type == \"sql\":\n            update_json = json.dumps(\n                {\n                    \"question\": question,\n                    \"sql\": update_content,\n                }\n            )\n        elif train_type == \"ddl\":\n            update_json = json.dumps(\n                {\n                    \"question\": None,\n                    \"ddl\": update_content,\n                }\n            )\n        elif train_type == \"documentation\":\n            update_json = json.dumps(\n                {\n                    \"question\": None,\n                    \"documentation\": update_content,\n                }\n            )\n        else:\n            update_json = json.dumps(\n                {\n                    \"question\": question,\n                    \"sql\": update_content,\n                }\n            )\n        cursor = self.oracle_conn.cursor()\n        cursor.setinputsizes(oracledb.DB_TYPE_VECTOR, oracledb.DB_TYPE_JSON)\n        cursor.execute(\n            \"\"\"\n                  UPDATE\n                      oracle_embedding\n                  SET\n                      embedding = TO_VECTOR(:1),\n                      document = JSON_MERGEPATCH(document, :2)\n                  WHERE\n                  uuid  = :3\n                  \"\"\",\n            [self.generate_embedding(update_content), update_json, id],\n        )\n\n        self.oracle_conn.commit()\n        cursor.close()\n        return True\n\n    @staticmethod\n    def _extract_documents(query_results) -> list:\n        \"\"\"\n        Static method to extract the documents from the results of a query.\n\n        Args:\n            query_results (pd.DataFrame): The dataframe to use.\n\n        Returns:\n            List[str] or None: The extracted documents, or an empty list or single document if an error occurred.\n        \"\"\"\n        if query_results is None or len(query_results) == 0:\n            return []\n\n        documents = [\n            json.loads(row_data[0]) if isinstance(row_data[0], str) else row_data[0]\n            for row_data in query_results\n        ]\n\n        return documents\n\n    def get_similar_question_sql(self, question: str, **kwargs) -> list:\n        embeddings = self.generate_embedding(question)\n        collection = self.get_collection(self.sql_collection)\n        cursor = self.oracle_conn.cursor()\n        cursor.setinputsizes(None, oracledb.DB_TYPE_VECTOR, oracledb.DB_TYPE_VECTOR)\n        cursor.execute(\n            \"\"\"\n      SELECT document\n      FROM oracle_embedding\n      WHERE collection_id = :1\n      ORDER BY VECTOR_DISTANCE(embedding, TO_VECTOR(:2), COSINE)\n      FETCH FIRST :3 ROWS ONLY\n      \"\"\",\n            [collection[\"uuid\"], embeddings, self.n_results_sql],\n        )\n        results = cursor.fetchall()\n        cursor.close()\n        return self._extract_documents(results)\n\n    def get_related_ddl(self, question: str, **kwargs) -> list:\n        collection = self.get_collection(self.ddl_collection)\n        cursor = self.oracle_conn.cursor()\n        cursor.setinputsizes(None, oracledb.DB_TYPE_VECTOR)\n        cursor.execute(\n            \"\"\"\n          SELECT\n              document\n          FROM oracle_embedding\n          WHERE\n              collection_id = :1\n          ORDER BY VECTOR_DISTANCE(embedding, TO_VECTOR(:2), COSINE)\n          FETCH FIRST :top_k ROWS ONLY\n      \"\"\",\n            [collection[\"uuid\"], self.generate_embedding(question), 100],\n        )\n        results = cursor.fetchall()\n\n        self.oracle_conn.commit()\n        cursor.close()\n        return Oracle_VectorStore._extract_documents(results)\n\n    def search_tables_metadata(\n        self,\n        engine: str = None,\n        catalog: str = None,\n        schema: str = None,\n        table_name: str = None,\n        ddl: str = None,\n        size: int = 10,\n        **kwargs,\n    ) -> list:\n        pass\n\n    def get_related_documentation(self, question: str, **kwargs) -> list:\n        collection = self.get_collection(self.documentation_collection)\n        cursor = self.oracle_conn.cursor()\n        cursor.setinputsizes(None, oracledb.DB_TYPE_VECTOR)\n        cursor.execute(\n            \"\"\"\n          SELECT\n              document\n          FROM oracle_embedding\n          WHERE\n              collection_id = :1\n          ORDER BY VECTOR_DISTANCE(embedding, TO_VECTOR(:2), DOT)\n          FETCH FIRST :top_k ROWS ONLY\n      \"\"\",\n            [collection[\"uuid\"], self.generate_embedding(question), 100],\n        )\n        results = cursor.fetchall()\n\n        self.oracle_conn.commit()\n        cursor.close()\n\n        return Oracle_VectorStore._extract_documents(results)\n\n    def create_tables_if_not_exists(self) -> None:\n        cursor = self.oracle_conn.cursor()\n        cursor.execute(\n            \"\"\"\n          CREATE TABLE IF NOT EXISTS oracle_collection (\n              name      VARCHAR2(200) NOT NULL,\n              cmetadata json NOT NULL,\n              uuid      VARCHAR2(200) NOT NULL,\n              CONSTRAINT oc_key_uuid PRIMARY KEY ( uuid )\n          )\n      \"\"\"\n        )\n\n        cursor.execute(\n            \"\"\"\n          CREATE TABLE IF NOT EXISTS oracle_embedding (\n              collection_id VARCHAR2(200) NOT NULL,\n              embedding     vector NOT NULL,\n              document      json NOT NULL,\n              cmetadata     json NOT NULL,\n              custom_id     VARCHAR2(200) NOT NULL,\n              uuid          VARCHAR2(200) NOT NULL,\n              CONSTRAINT oe_key_uuid PRIMARY KEY ( uuid )\n          )\n      \"\"\"\n        )\n\n        self.oracle_conn.commit()\n        cursor.close()\n\n    def create_collections_if_not_exists(\n        self,\n        name: str,\n        cmetadata: Optional[dict] = None,\n    ) -> Tuple[dict, bool]:\n        \"\"\"\n        Get or create a collection.\n        Returns [Collection, bool] where the bool is True if the collection was created.\n        \"\"\"\n        if self.pre_delete_collection:\n            self.delete_collection(name)\n        created = False\n        collection = self.get_collection(name)\n        if collection:\n            return collection, created\n\n        cmetadata = (\n            json.dumps(self.cmetadata) if cmetadata is None else json.dumps(cmetadata)\n        )\n        collection_id = str(uuid.uuid4())\n        cursor = self.oracle_conn.cursor()\n        cursor.execute(\n            \"\"\"\n          INSERT INTO oracle_collection(name, cmetadata, uuid)\n          VALUES (:1, :2, :3)\n      \"\"\",\n            [name, cmetadata, str(collection_id)],\n        )\n\n        self.oracle_conn.commit()\n        cursor.close()\n\n        collection = {\"name\": name, \"cmetadata\": cmetadata, \"uuid\": collection_id}\n        created = True\n        return collection, created\n\n    def get_collection(self, name) -> Optional[dict]:\n        return self.get_by_name(name)\n\n    def get_by_name(self, name: str) -> Optional[dict]:\n        cursor = self.oracle_conn.cursor()\n        cursor.execute(\n            \"\"\"\n          SELECT\n              name,\n              cmetadata,\n              uuid\n          FROM\n              oracle_collection\n          WHERE\n              name = :1\n          FETCH FIRST 1 ROWS ONLY\n      \"\"\",\n            [name],\n        )\n\n        for row in cursor:\n            return {\"name\": row[0], \"cmetadata\": row[1], \"uuid\": row[2]}\n\n        return  # type: ignore\n\n    def delete_collection(self, name) -> None:\n        collection = self.get_collection(name)\n        if not collection:\n            return\n\n        cursor = self.oracle_conn.cursor()\n        cursor.execute(\n            \"\"\"\n          DELETE\n          FROM\n              oracle_embedding\n          WHERE\n          collection_id = ( SELECT uuid FROM oracle_collection WHERE name = :1 )\n      \"\"\",\n            [name],\n        )\n        cursor.execute(\n            \"\"\"\n          DELETE\n          FROM\n              oracle_collection\n          WHERE\n              name = :1\n      \"\"\",\n            [name],\n        )\n\n        self.oracle_conn.commit()\n        cursor.close()\n"
  },
  {
    "path": "src/vanna/legacy/pgvector/__init__.py",
    "content": "from .pgvector import PG_VectorStore\n"
  },
  {
    "path": "src/vanna/legacy/pgvector/pgvector.py",
    "content": "import ast\nimport json\nimport logging\nimport uuid\n\nimport pandas as pd\nfrom langchain_core.documents import Document\nfrom langchain_postgres.vectorstores import PGVector\nfrom sqlalchemy import create_engine, text\n\nfrom .. import ValidationError\nfrom ..base import VannaBase\nfrom ..types import TrainingPlan, TrainingPlanItem\n\n\nclass PG_VectorStore(VannaBase):\n    def __init__(self, config=None):\n        if not config or \"connection_string\" not in config:\n            raise ValueError(\n                \"A valid 'config' dictionary with a 'connection_string' is required.\"\n            )\n\n        VannaBase.__init__(self, config=config)\n\n        if config and \"connection_string\" in config:\n            self.connection_string = config.get(\"connection_string\")\n            self.n_results = config.get(\"n_results\", 10)\n\n        if config and \"embedding_function\" in config:\n            self.embedding_function = config.get(\"embedding_function\")\n        else:\n            from langchain_huggingface import HuggingFaceEmbeddings\n\n            self.embedding_function = HuggingFaceEmbeddings(\n                model_name=\"all-MiniLM-L6-v2\"\n            )\n\n        self.sql_collection = PGVector(\n            embeddings=self.embedding_function,\n            collection_name=\"sql\",\n            connection=self.connection_string,\n        )\n        self.ddl_collection = PGVector(\n            embeddings=self.embedding_function,\n            collection_name=\"ddl\",\n            connection=self.connection_string,\n        )\n        self.documentation_collection = PGVector(\n            embeddings=self.embedding_function,\n            collection_name=\"documentation\",\n            connection=self.connection_string,\n        )\n\n    def add_question_sql(self, question: str, sql: str, **kwargs) -> str:\n        question_sql_json = json.dumps(\n            {\n                \"question\": question,\n                \"sql\": sql,\n            },\n            ensure_ascii=False,\n        )\n        id = str(uuid.uuid4()) + \"-sql\"\n        createdat = kwargs.get(\"createdat\")\n        doc = Document(\n            page_content=question_sql_json,\n            metadata={\"id\": id, \"createdat\": createdat},\n        )\n        self.sql_collection.add_documents([doc], ids=[doc.metadata[\"id\"]])\n\n        return id\n\n    def add_ddl(self, ddl: str, **kwargs) -> str:\n        _id = str(uuid.uuid4()) + \"-ddl\"\n        doc = Document(\n            page_content=ddl,\n            metadata={\"id\": _id},\n        )\n        self.ddl_collection.add_documents([doc], ids=[doc.metadata[\"id\"]])\n        return _id\n\n    def add_documentation(self, documentation: str, **kwargs) -> str:\n        _id = str(uuid.uuid4()) + \"-doc\"\n        doc = Document(\n            page_content=documentation,\n            metadata={\"id\": _id},\n        )\n        self.documentation_collection.add_documents([doc], ids=[doc.metadata[\"id\"]])\n        return _id\n\n    def get_collection(self, collection_name):\n        match collection_name:\n            case \"sql\":\n                return self.sql_collection\n            case \"ddl\":\n                return self.ddl_collection\n            case \"documentation\":\n                return self.documentation_collection\n            case _:\n                raise ValueError(\"Specified collection does not exist.\")\n\n    def get_similar_question_sql(self, question: str) -> list:\n        documents = self.sql_collection.similarity_search(\n            query=question, k=self.n_results\n        )\n        return [ast.literal_eval(document.page_content) for document in documents]\n\n    def get_related_ddl(self, question: str, **kwargs) -> list:\n        documents = self.ddl_collection.similarity_search(\n            query=question, k=self.n_results\n        )\n        return [document.page_content for document in documents]\n\n    def get_related_documentation(self, question: str, **kwargs) -> list:\n        documents = self.documentation_collection.similarity_search(\n            query=question, k=self.n_results\n        )\n        return [document.page_content for document in documents]\n\n    def train(\n        self,\n        question: str | None = None,\n        sql: str | None = None,\n        ddl: str | None = None,\n        documentation: str | None = None,\n        plan: TrainingPlan | None = None,\n        createdat: str | None = None,\n    ):\n        if question and not sql:\n            raise ValidationError(\"Please provide a SQL query.\")\n\n        if documentation:\n            logging.info(f\"Adding documentation: {documentation}\")\n            return self.add_documentation(documentation)\n\n        if sql and question:\n            return self.add_question_sql(\n                question=question, sql=sql, createdat=createdat\n            )\n\n        if ddl:\n            logging.info(f\"Adding ddl: {ddl}\")\n            return self.add_ddl(ddl)\n\n        if plan:\n            for item in plan._plan:\n                if item.item_type == TrainingPlanItem.ITEM_TYPE_DDL:\n                    self.add_ddl(item.item_value)\n                elif item.item_type == TrainingPlanItem.ITEM_TYPE_IS:\n                    self.add_documentation(item.item_value)\n                elif (\n                    item.item_type == TrainingPlanItem.ITEM_TYPE_SQL and item.item_name\n                ):\n                    self.add_question_sql(question=item.item_name, sql=item.item_value)\n\n    def get_training_data(self, **kwargs) -> pd.DataFrame:\n        # Establishing the connection\n        engine = create_engine(self.connection_string)\n\n        # Querying the 'langchain_pg_embedding' table\n        query_embedding = \"SELECT cmetadata, document FROM langchain_pg_embedding\"\n        df_embedding = pd.read_sql(query_embedding, engine)\n\n        # List to accumulate the processed rows\n        processed_rows = []\n\n        # Process each row in the DataFrame\n        for _, row in df_embedding.iterrows():\n            custom_id = row[\"cmetadata\"][\"id\"]\n            document = row[\"document\"]\n            training_data_type = (\n                \"documentation\" if custom_id[-3:] == \"doc\" else custom_id[-3:]\n            )\n\n            if training_data_type == \"sql\":\n                # Convert the document string to a dictionary\n                try:\n                    doc_dict = ast.literal_eval(document)\n                    question = doc_dict.get(\"question\")\n                    content = doc_dict.get(\"sql\")\n                except (ValueError, SyntaxError):\n                    logging.info(\n                        f\"Skipping row with custom_id {custom_id} due to parsing error.\"\n                    )\n                    continue\n            elif training_data_type in [\"documentation\", \"ddl\"]:\n                question = None  # Default value for question\n                content = document\n            else:\n                # If the suffix is not recognized, skip this row\n                logging.info(\n                    f\"Skipping row with custom_id {custom_id} due to unrecognized training data type.\"\n                )\n                continue\n\n            # Append the processed data to the list\n            processed_rows.append(\n                {\n                    \"id\": custom_id,\n                    \"question\": question,\n                    \"content\": content,\n                    \"training_data_type\": training_data_type,\n                }\n            )\n\n        # Create a DataFrame from the list of processed rows\n        df_processed = pd.DataFrame(processed_rows)\n\n        return df_processed\n\n    def remove_training_data(self, id: str, **kwargs) -> bool:\n        # Create the database engine\n        engine = create_engine(self.connection_string)\n\n        # SQL DELETE statement\n        delete_statement = text(\n            \"\"\"\n            DELETE FROM langchain_pg_embedding\n            WHERE cmetadata ->> 'id' = :id\n        \"\"\"\n        )\n\n        # Connect to the database and execute the delete statement\n        with engine.connect() as connection:\n            # Start a transaction\n            with connection.begin() as transaction:\n                try:\n                    result = connection.execute(delete_statement, {\"id\": id})\n                    # Commit the transaction if the delete was successful\n                    transaction.commit()\n                    # Check if any row was deleted and return True or False accordingly\n                    return result.rowcount > 0\n                except Exception as e:\n                    # Rollback the transaction in case of error\n                    logging.error(f\"An error occurred: {e}\")\n                    transaction.rollback()\n                    return False\n\n    def remove_collection(self, collection_name: str) -> bool:\n        engine = create_engine(self.connection_string)\n\n        # Determine the suffix to look for based on the collection name\n        suffix_map = {\"ddl\": \"ddl\", \"sql\": \"sql\", \"documentation\": \"doc\"}\n        suffix = suffix_map.get(collection_name)\n\n        if not suffix:\n            logging.info(\n                \"Invalid collection name. Choose from 'ddl', 'sql', or 'documentation'.\"\n            )\n            return False\n\n        # SQL query to delete rows based on the condition\n        query = text(\n            f\"\"\"\n            DELETE FROM langchain_pg_embedding\n            WHERE cmetadata->>'id' LIKE '%{suffix}'\n        \"\"\"\n        )\n\n        # Execute the deletion within a transaction block\n        with engine.connect() as connection:\n            with connection.begin() as transaction:\n                try:\n                    result = connection.execute(query)\n                    transaction.commit()  # Explicitly commit the transaction\n                    if result.rowcount > 0:\n                        logging.info(\n                            f\"Deleted {result.rowcount} rows from \"\n                            f\"langchain_pg_embedding where collection is {collection_name}.\"\n                        )\n                        return True\n                    else:\n                        logging.info(\n                            f\"No rows deleted for collection {collection_name}.\"\n                        )\n                        return False\n                except Exception as e:\n                    logging.error(f\"An error occurred: {e}\")\n                    transaction.rollback()  # Rollback in case of error\n                    return False\n\n    def generate_embedding(self, *args, **kwargs):\n        pass\n"
  },
  {
    "path": "src/vanna/legacy/pinecone/__init__.py",
    "content": "from .pinecone_vector import PineconeDB_VectorStore\n\n__all__ = [\"PineconeDB_VectorStore\"]\n"
  },
  {
    "path": "src/vanna/legacy/pinecone/pinecone_vector.py",
    "content": "import json\nfrom typing import List\n\nfrom pinecone import Pinecone, PodSpec, ServerlessSpec\nimport pandas as pd\nfrom ..base import VannaBase\nfrom ..utils import deterministic_uuid\n\nfrom fastembed import TextEmbedding\n\n\nclass PineconeDB_VectorStore(VannaBase):\n    \"\"\"\n    Vectorstore using PineconeDB\n\n    Args:\n        config (dict): Configuration dictionary. Defaults to {}. You must provide either a Pinecone Client or an API key in the config.\n            - client (Pinecone, optional): Pinecone client. Defaults to None.\n            - api_key (str, optional): Pinecone API key. Defaults to None.\n            - n_results (int, optional): Number of results to return. Defaults to 10.\n            - dimensions (int, optional): Dimensions of the embeddings. Defaults to 384 which coresponds to the dimensions of BAAI/bge-small-en-v1.5.\n            - fastembed_model (str, optional): Fastembed model to use. Defaults to \"BAAI/bge-small-en-v1.5\".\n            - documentation_namespace (str, optional): Namespace for documentation. Defaults to \"documentation\".\n            - distance_metric (str, optional): Distance metric to use. Defaults to \"cosine\".\n            - ddl_namespace (str, optional): Namespace for DDL. Defaults to \"ddl\".\n            - sql_namespace (str, optional): Namespace for SQL. Defaults to \"sql\".\n            - index_name (str, optional): Name of the index. Defaults to \"vanna-index\".\n            - metadata_config (dict, optional): Metadata configuration if using a pinecone pod. Defaults to {}.\n            - server_type (str, optional): Type of Pinecone server to use. Defaults to \"serverless\". Options are \"serverless\" or \"pod\".\n            - podspec (PodSpec, optional): PodSpec configuration if using a pinecone pod. Defaults to PodSpec(environment=\"us-west-2\", pod_type=\"p1.x1\", metadata_config=self.metadata_config).\n            - serverless_spec (ServerlessSpec, optional): ServerlessSpec configuration if using a pinecone serverless index. Defaults to ServerlessSpec(cloud=\"aws\", region=\"us-west-2\").\n    Raises:\n        ValueError: If config is None, api_key is not provided OR client is not provided, client is not an instance of Pinecone, or server_type is not \"serverless\" or \"pod\".\n    \"\"\"\n\n    def __init__(self, config=None):\n        VannaBase.__init__(self, config=config)\n        if config is None:\n            raise ValueError(\n                \"config is required, pass either a Pinecone client or an API key in the config.\"\n            )\n        client = config.get(\"client\")\n        api_key = config.get(\"api_key\")\n        if not api_key and not client:\n            raise ValueError(\n                \"api_key is required in config or pass a configured client\"\n            )\n        if not client and api_key:\n            self._client = Pinecone(api_key=api_key)\n        elif not isinstance(client, Pinecone):\n            raise ValueError(\"client must be an instance of Pinecone\")\n        else:\n            self._client = client\n\n        self.n_results = config.get(\"n_results\", 10)\n        self.dimensions = config.get(\"dimensions\", 384)\n        self.fastembed_model = config.get(\"fastembed_model\", \"BAAI/bge-small-en-v1.5\")\n        self.documentation_namespace = config.get(\n            \"documentation_namespace\", \"documentation\"\n        )\n        self.distance_metric = config.get(\"distance_metric\", \"cosine\")\n        self.ddl_namespace = config.get(\"ddl_namespace\", \"ddl\")\n        self.sql_namespace = config.get(\"sql_namespace\", \"sql\")\n        self.index_name = config.get(\"index_name\", \"vanna-index\")\n        self.metadata_config = config.get(\"metadata_config\", {})\n        self.server_type = config.get(\"server_type\", \"serverless\")\n        if self.server_type not in [\"serverless\", \"pod\"]:\n            raise ValueError(\"server_type must be either 'serverless' or 'pod'\")\n        self.podspec = config.get(\n            \"podspec\",\n            PodSpec(\n                environment=\"us-west-2\",\n                pod_type=\"p1.x1\",\n                metadata_config=self.metadata_config,\n            ),\n        )\n        self.serverless_spec = config.get(\n            \"serverless_spec\", ServerlessSpec(cloud=\"aws\", region=\"us-west-2\")\n        )\n        self._setup_index()\n\n    def _set_index_host(self, host: str) -> None:\n        self.Index = self._client.Index(host=host)\n\n    def _setup_index(self) -> None:\n        existing_indexes = self._get_indexes()\n        if self.index_name not in existing_indexes and self.server_type == \"serverless\":\n            self._client.create_index(\n                name=self.index_name,\n                dimension=self.dimensions,\n                metric=self.distance_metric,\n                spec=self.serverless_spec,\n            )\n            pinecone_index_host = self._client.describe_index(self.index_name)[\"host\"]\n            self._set_index_host(pinecone_index_host)\n        elif self.index_name not in existing_indexes and self.server_type == \"pod\":\n            self._client.create_index(\n                name=self.index_name,\n                dimension=self.dimensions,\n                metric=self.distance_metric,\n                spec=self.podspec,\n            )\n            pinecone_index_host = self._client.describe_index(self.index_name)[\"host\"]\n            self._set_index_host(pinecone_index_host)\n        else:\n            pinecone_index_host = self._client.describe_index(self.index_name)[\"host\"]\n            self._set_index_host(pinecone_index_host)\n\n    def _get_indexes(self) -> list:\n        return [index[\"name\"] for index in self._client.list_indexes()]\n\n    def _check_if_embedding_exists(self, id: str, namespace: str) -> bool:\n        fetch_response = self.Index.fetch(ids=[id], namespace=namespace)\n        if fetch_response.vectors == {}:\n            return False\n        return True\n\n    def add_ddl(self, ddl: str, **kwargs) -> str:\n        id = deterministic_uuid(ddl) + \"-ddl\"\n        if self._check_if_embedding_exists(id=id, namespace=self.ddl_namespace):\n            print(f\"DDL with id: {id} already exists in the index. Skipping...\")\n            return id\n        self.Index.upsert(\n            vectors=[(id, self.generate_embedding(ddl), {\"ddl\": ddl})],\n            namespace=self.ddl_namespace,\n        )\n        return id\n\n    def add_documentation(self, doc: str, **kwargs) -> str:\n        id = deterministic_uuid(doc) + \"-doc\"\n\n        if self._check_if_embedding_exists(\n            id=id, namespace=self.documentation_namespace\n        ):\n            print(\n                f\"Documentation with id: {id} already exists in the index. Skipping...\"\n            )\n            return id\n        self.Index.upsert(\n            vectors=[(id, self.generate_embedding(doc), {\"documentation\": doc})],\n            namespace=self.documentation_namespace,\n        )\n        return id\n\n    def add_question_sql(self, question: str, sql: str, **kwargs) -> str:\n        question_sql_json = json.dumps(\n            {\n                \"question\": question,\n                \"sql\": sql,\n            },\n            ensure_ascii=False,\n        )\n        id = deterministic_uuid(question_sql_json) + \"-sql\"\n        if self._check_if_embedding_exists(id=id, namespace=self.sql_namespace):\n            print(\n                f\"Question-SQL with id: {id} already exists in the index. Skipping...\"\n            )\n            return id\n        self.Index.upsert(\n            vectors=[\n                (\n                    id,\n                    self.generate_embedding(question_sql_json),\n                    {\"sql\": question_sql_json},\n                )\n            ],\n            namespace=self.sql_namespace,\n        )\n        return id\n\n    def get_related_ddl(self, question: str, **kwargs) -> list:\n        res = self.Index.query(\n            namespace=self.ddl_namespace,\n            vector=self.generate_embedding(question),\n            top_k=self.n_results,\n            include_values=True,\n            include_metadata=True,\n        )\n        return [match[\"metadata\"][\"ddl\"] for match in res[\"matches\"]] if res else []\n\n    def get_related_documentation(self, question: str, **kwargs) -> list:\n        res = self.Index.query(\n            namespace=self.documentation_namespace,\n            vector=self.generate_embedding(question),\n            top_k=self.n_results,\n            include_values=True,\n            include_metadata=True,\n        )\n        return (\n            [match[\"metadata\"][\"documentation\"] for match in res[\"matches\"]]\n            if res\n            else []\n        )\n\n    def get_similar_question_sql(self, question: str, **kwargs) -> list:\n        res = self.Index.query(\n            namespace=self.sql_namespace,\n            vector=self.generate_embedding(question),\n            top_k=self.n_results,\n            include_values=True,\n            include_metadata=True,\n        )\n        return (\n            [\n                {\n                    key: value\n                    for key, value in json.loads(match[\"metadata\"][\"sql\"]).items()\n                }\n                for match in res[\"matches\"]\n            ]\n            if res\n            else []\n        )\n\n    def get_training_data(self, **kwargs) -> pd.DataFrame:\n        # Pinecone does not support getting all vectors in a namespace, so we have to query for the top_k vectors with a dummy vector\n        df = pd.DataFrame()\n        namespaces = {\n            \"sql\": self.sql_namespace,\n            \"ddl\": self.ddl_namespace,\n            \"documentation\": self.documentation_namespace,\n        }\n\n        for data_type, namespace in namespaces.items():\n            data = self.Index.query(\n                top_k=10000,  # max results that pinecone allows\n                namespace=namespace,\n                include_values=True,\n                include_metadata=True,\n                vector=[0.0] * self.dimensions,\n            )\n\n            if data is not None:\n                id_list = [match[\"id\"] for match in data[\"matches\"]]\n                content_list = [\n                    match[\"metadata\"][data_type] for match in data[\"matches\"]\n                ]\n                question_list = [\n                    (\n                        json.loads(match[\"metadata\"][data_type])[\"question\"]\n                        if data_type == \"sql\"\n                        else None\n                    )\n                    for match in data[\"matches\"]\n                ]\n\n                df_data = pd.DataFrame(\n                    {\n                        \"id\": id_list,\n                        \"question\": question_list,\n                        \"content\": content_list,\n                    }\n                )\n                df_data[\"training_data_type\"] = data_type\n                df = pd.concat([df, df_data])\n\n        return df\n\n    def remove_training_data(self, id: str, **kwargs) -> bool:\n        if id.endswith(\"-sql\"):\n            self.Index.delete(ids=[id], namespace=self.sql_namespace)\n            return True\n        elif id.endswith(\"-ddl\"):\n            self.Index.delete(ids=[id], namespace=self.ddl_namespace)\n            return True\n        elif id.endswith(\"-doc\"):\n            self.Index.delete(ids=[id], namespace=self.documentation_namespace)\n            return True\n        else:\n            return False\n\n    def generate_embedding(self, data: str, **kwargs) -> List[float]:\n        embedding_model = TextEmbedding(model_name=self.fastembed_model)\n        embedding = next(embedding_model.embed(data))\n        return embedding.tolist()\n"
  },
  {
    "path": "src/vanna/legacy/qdrant/__init__.py",
    "content": "from .qdrant import Qdrant_VectorStore\n\n__all__ = [\"Qdrant_VectorStore\"]\n"
  },
  {
    "path": "src/vanna/legacy/qdrant/qdrant.py",
    "content": "from functools import cached_property\nfrom typing import List, Tuple\n\nimport pandas as pd\nfrom qdrant_client import QdrantClient, grpc, models\n\nfrom ..base import VannaBase\nfrom ..utils import deterministic_uuid\n\nSCROLL_SIZE = 1000\n\n\nclass Qdrant_VectorStore(VannaBase):\n    \"\"\"\n    Vectorstore implementation using Qdrant - https://qdrant.tech/\n\n    Args:\n        - config (dict, optional): Dictionary of `Qdrant_VectorStore config` options. Defaults to `{}`.\n            - client: A `qdrant_client.QdrantClient` instance. Overrides other config options.\n            - location: If `\":memory:\"` - use in-memory Qdrant instance. If `str` - use it as a `url` parameter.\n            - url: Either host or str of \"Optional[scheme], host, Optional[port], Optional[prefix]\". Eg. `\"http://localhost:6333\"`.\n            - prefer_grpc: If `true` - use gPRC interface whenever possible in custom methods.\n            - https: If `true` - use HTTPS(SSL) protocol. Default: `None`\n            - api_key: API key for authentication in Qdrant Cloud. Default: `None`\n            - timeout: Timeout for REST and gRPC API requests. Defaults to 5 seconds for REST and unlimited for gRPC.\n            - path: Persistence path for QdrantLocal. Default: `None`.\n            - prefix: Prefix to the REST URL paths. Example: `service/v1` will result in `http://localhost:6333/service/v1/{qdrant-endpoint}`.\n            - n_results: Number of results to return from similarity search. Defaults to 10.\n            - fastembed_model: [Model](https://qdrant.github.io/fastembed/examples/Supported_Models/#supported-text-embedding-models) to use for `fastembed.TextEmbedding`.\n              Defaults to `\"BAAI/bge-small-en-v1.5\"`.\n            - collection_params: Additional parameters to pass to `qdrant_client.QdrantClient#create_collection()` method.\n            - distance_metric: Distance metric to use when creating collections. Defaults to `qdrant_client.models.Distance.COSINE`.\n            - documentation_collection_name: Name of the collection to store documentation. Defaults to `\"documentation\"`.\n            - ddl_collection_name: Name of the collection to store DDL. Defaults to `\"ddl\"`.\n            - sql_collection_name: Name of the collection to store SQL. Defaults to `\"sql\"`.\n\n    Raises:\n        TypeError: If config[\"client\"] is not a `qdrant_client.QdrantClient` instance\n    \"\"\"\n\n    def __init__(\n        self,\n        config={},\n    ):\n        VannaBase.__init__(self, config=config)\n        client = config.get(\"client\")\n\n        if client is None:\n            self._client = QdrantClient(\n                location=config.get(\"location\", None),\n                url=config.get(\"url\", None),\n                prefer_grpc=config.get(\"prefer_grpc\", False),\n                https=config.get(\"https\", None),\n                api_key=config.get(\"api_key\", None),\n                timeout=config.get(\"timeout\", None),\n                path=config.get(\"path\", None),\n                prefix=config.get(\"prefix\", None),\n            )\n        elif not isinstance(client, QdrantClient):\n            raise TypeError(\n                f\"Unsupported client of type {client.__class__} was set in config\"\n            )\n\n        else:\n            self._client = client\n\n        self.n_results = config.get(\"n_results\", 10)\n        self.fastembed_model = config.get(\"fastembed_model\", \"BAAI/bge-small-en-v1.5\")\n        self.collection_params = config.get(\"collection_params\", {})\n        self.distance_metric = config.get(\"distance_metric\", models.Distance.COSINE)\n        self.documentation_collection_name = config.get(\n            \"documentation_collection_name\", \"documentation\"\n        )\n        self.ddl_collection_name = config.get(\"ddl_collection_name\", \"ddl\")\n        self.sql_collection_name = config.get(\"sql_collection_name\", \"sql\")\n\n        self.id_suffixes = {\n            self.ddl_collection_name: \"ddl\",\n            self.documentation_collection_name: \"doc\",\n            self.sql_collection_name: \"sql\",\n        }\n\n        self._setup_collections()\n\n    def add_question_sql(self, question: str, sql: str, **kwargs) -> str:\n        question_answer = \"Question: {0}\\n\\nSQL: {1}\".format(question, sql)\n        id = deterministic_uuid(question_answer)\n\n        self._client.upsert(\n            self.sql_collection_name,\n            points=[\n                models.PointStruct(\n                    id=id,\n                    vector=self.generate_embedding(question_answer),\n                    payload={\n                        \"question\": question,\n                        \"sql\": sql,\n                    },\n                )\n            ],\n        )\n\n        return self._format_point_id(id, self.sql_collection_name)\n\n    def add_ddl(self, ddl: str, **kwargs) -> str:\n        id = deterministic_uuid(ddl)\n        self._client.upsert(\n            self.ddl_collection_name,\n            points=[\n                models.PointStruct(\n                    id=id,\n                    vector=self.generate_embedding(ddl),\n                    payload={\n                        \"ddl\": ddl,\n                    },\n                )\n            ],\n        )\n        return self._format_point_id(id, self.ddl_collection_name)\n\n    def add_documentation(self, documentation: str, **kwargs) -> str:\n        id = deterministic_uuid(documentation)\n\n        self._client.upsert(\n            self.documentation_collection_name,\n            points=[\n                models.PointStruct(\n                    id=id,\n                    vector=self.generate_embedding(documentation),\n                    payload={\n                        \"documentation\": documentation,\n                    },\n                )\n            ],\n        )\n\n        return self._format_point_id(id, self.documentation_collection_name)\n\n    def get_training_data(self, **kwargs) -> pd.DataFrame:\n        df = pd.DataFrame()\n\n        if sql_data := self._get_all_points(self.sql_collection_name):\n            question_list = [data.payload[\"question\"] for data in sql_data]\n            sql_list = [data.payload[\"sql\"] for data in sql_data]\n            id_list = [\n                self._format_point_id(data.id, self.sql_collection_name)\n                for data in sql_data\n            ]\n\n            df_sql = pd.DataFrame(\n                {\n                    \"id\": id_list,\n                    \"question\": question_list,\n                    \"content\": sql_list,\n                }\n            )\n\n            df_sql[\"training_data_type\"] = \"sql\"\n\n            df = pd.concat([df, df_sql])\n\n        if ddl_data := self._get_all_points(self.ddl_collection_name):\n            ddl_list = [data.payload[\"ddl\"] for data in ddl_data]\n            id_list = [\n                self._format_point_id(data.id, self.ddl_collection_name)\n                for data in ddl_data\n            ]\n\n            df_ddl = pd.DataFrame(\n                {\n                    \"id\": id_list,\n                    \"question\": [None for _ in ddl_list],\n                    \"content\": ddl_list,\n                }\n            )\n\n            df_ddl[\"training_data_type\"] = \"ddl\"\n\n            df = pd.concat([df, df_ddl])\n\n        if doc_data := self._get_all_points(self.documentation_collection_name):\n            document_list = [data.payload[\"documentation\"] for data in doc_data]\n            id_list = [\n                self._format_point_id(data.id, self.documentation_collection_name)\n                for data in doc_data\n            ]\n\n            df_doc = pd.DataFrame(\n                {\n                    \"id\": id_list,\n                    \"question\": [None for _ in document_list],\n                    \"content\": document_list,\n                }\n            )\n\n            df_doc[\"training_data_type\"] = \"documentation\"\n\n            df = pd.concat([df, df_doc])\n\n        return df\n\n    def remove_training_data(self, id: str, **kwargs) -> bool:\n        try:\n            id, collection_name = self._parse_point_id(id)\n            res = self._client.delete(collection_name, points_selector=[id])\n            return True\n        except ValueError:\n            return False\n\n    def remove_collection(self, collection_name: str) -> bool:\n        \"\"\"\n        This function can reset the collection to empty state.\n\n        Args:\n            collection_name (str): sql or ddl or documentation\n\n        Returns:\n            bool: True if collection is deleted, False otherwise\n        \"\"\"\n        if collection_name in self.id_suffixes.keys():\n            self._client.delete_collection(collection_name)\n            self._setup_collections()\n            return True\n        else:\n            return False\n\n    @cached_property\n    def embeddings_dimension(self):\n        return len(self.generate_embedding(\"ABCDEF\"))\n\n    def get_similar_question_sql(self, question: str, **kwargs) -> list:\n        results = self._client.query_points(\n            self.sql_collection_name,\n            query=self.generate_embedding(question),\n            limit=self.n_results,\n            with_payload=True,\n        ).points\n\n        return [dict(result.payload) for result in results]\n\n    def get_related_ddl(self, question: str, **kwargs) -> list:\n        results = self._client.query_points(\n            self.ddl_collection_name,\n            query=self.generate_embedding(question),\n            limit=self.n_results,\n            with_payload=True,\n        ).points\n\n        return [result.payload[\"ddl\"] for result in results]\n\n    def get_related_documentation(self, question: str, **kwargs) -> list:\n        results = self._client.query_points(\n            self.documentation_collection_name,\n            query=self.generate_embedding(question),\n            limit=self.n_results,\n            with_payload=True,\n        ).points\n\n        return [result.payload[\"documentation\"] for result in results]\n\n    def generate_embedding(self, data: str, **kwargs) -> List[float]:\n        embedding_model = self._client._get_or_init_model(\n            model_name=self.fastembed_model\n        )\n        embedding = next(embedding_model.embed(data))\n\n        return embedding.tolist()\n\n    def _get_all_points(self, collection_name: str):\n        results: List[models.Record] = []\n        next_offset = None\n        stop_scrolling = False\n        while not stop_scrolling:\n            records, next_offset = self._client.scroll(\n                collection_name,\n                limit=SCROLL_SIZE,\n                offset=next_offset,\n                with_payload=True,\n                with_vectors=False,\n            )\n            stop_scrolling = next_offset is None or (\n                isinstance(next_offset, grpc.PointId)\n                and next_offset.num == 0\n                and next_offset.uuid == \"\"\n            )\n\n            results.extend(records)\n\n        return results\n\n    def _setup_collections(self):\n        if not self._client.collection_exists(self.sql_collection_name):\n            self._client.create_collection(\n                collection_name=self.sql_collection_name,\n                vectors_config=models.VectorParams(\n                    size=self.embeddings_dimension,\n                    distance=self.distance_metric,\n                ),\n                **self.collection_params,\n            )\n\n        if not self._client.collection_exists(self.ddl_collection_name):\n            self._client.create_collection(\n                collection_name=self.ddl_collection_name,\n                vectors_config=models.VectorParams(\n                    size=self.embeddings_dimension,\n                    distance=self.distance_metric,\n                ),\n                **self.collection_params,\n            )\n        if not self._client.collection_exists(self.documentation_collection_name):\n            self._client.create_collection(\n                collection_name=self.documentation_collection_name,\n                vectors_config=models.VectorParams(\n                    size=self.embeddings_dimension,\n                    distance=self.distance_metric,\n                ),\n                **self.collection_params,\n            )\n\n    def _format_point_id(self, id: str, collection_name: str) -> str:\n        return \"{0}-{1}\".format(id, self.id_suffixes[collection_name])\n\n    def _parse_point_id(self, id: str) -> Tuple[str, str]:\n        id, curr_suffix = id.rsplit(\"-\", 1)\n        for collection_name, suffix in self.id_suffixes.items():\n            if curr_suffix == suffix:\n                return id, collection_name\n        raise ValueError(f\"Invalid id {id}\")\n"
  },
  {
    "path": "src/vanna/legacy/qianfan/Qianfan_Chat.py",
    "content": "import qianfan\n\nfrom ..base import VannaBase\n\n\nclass Qianfan_Chat(VannaBase):\n    def __init__(self, client=None, config=None):\n        VannaBase.__init__(self, config=config)\n\n        if \"api_key\" not in config:\n            raise Exception(\"Missing api_key in config\")\n        self.api_key = config[\"api_key\"]\n\n        if \"secret_key\" not in config:\n            raise Exception(\"Missing secret_key in config\")\n        self.secret_key = config[\"secret_key\"]\n\n        # default parameters - can be overrided using config\n        self.temperature = 0.9\n        self.max_tokens = 1024\n\n        if \"temperature\" in config:\n            self.temperature = config[\"temperature\"]\n\n        if \"max_tokens\" in config:\n            self.max_tokens = config[\"max_tokens\"]\n\n        self.model = config[\"model\"] if \"model\" in config else \"ERNIE-Speed\"\n\n        if client is not None:\n            self.client = client\n            return\n\n        self.client = qianfan.ChatCompletion(ak=self.api_key, sk=self.secret_key)\n\n    def system_message(self, message: str) -> any:\n        return {\"role\": \"system\", \"content\": message}\n\n    def user_message(self, message: str) -> any:\n        return {\"role\": \"user\", \"content\": message}\n\n    def assistant_message(self, message: str) -> any:\n        return {\"role\": \"assistant\", \"content\": message}\n\n    def get_sql_prompt(\n        self,\n        initial_prompt: str,\n        question: str,\n        question_sql_list: list,\n        ddl_list: list,\n        doc_list: list,\n        **kwargs,\n    ):\n        \"\"\"\n        Example:\n        ```python\n        vn.get_sql_prompt(\n            question=\"What are the top 10 customers by sales?\",\n            question_sql_list=[{\"question\": \"What are the top 10 customers by sales?\", \"sql\": \"SELECT * FROM customers ORDER BY sales DESC LIMIT 10\"}],\n            ddl_list=[\"CREATE TABLE customers (id INT, name TEXT, sales DECIMAL)\"],\n            doc_list=[\"The customers table contains information about customers and their sales.\"],\n        )\n\n        ```\n\n        This method is used to generate a prompt for the LLM to generate SQL.\n\n        Args:\n            question (str): The question to generate SQL for.\n            question_sql_list (list): A list of questions and their corresponding SQL statements.\n            ddl_list (list): A list of DDL statements.\n            doc_list (list): A list of documentation.\n\n        Returns:\n            any: The prompt for the LLM to generate SQL.\n        \"\"\"\n\n        if initial_prompt is None:\n            initial_prompt = (\n                f\"You are a {self.dialect} expert. \"\n                + \"Please help to generate a SQL to answer the question based on some context.Please don't give any explanation for your answer. Just only generate a SQL \\n\"\n            )\n\n        initial_prompt = self.add_ddl_to_prompt(\n            initial_prompt, ddl_list, max_tokens=self.max_tokens\n        )\n\n        if self.static_documentation != \"\":\n            doc_list.append(self.static_documentation)\n\n        initial_prompt = self.add_documentation_to_prompt(\n            initial_prompt, doc_list, max_tokens=self.max_tokens\n        )\n        message_log = []\n\n        if question_sql_list is None or len(question_sql_list) == 0:\n            initial_prompt = initial_prompt + f\"question: {question}\"\n            message_log.append(self.user_message(initial_prompt))\n        else:\n            for i, example in question_sql_list:\n                if example is None:\n                    print(\"example is None\")\n                else:\n                    if (\n                        example is not None\n                        and \"question\" in example\n                        and \"sql\" in example\n                    ):\n                        if i == 0:\n                            initial_prompt = (\n                                initial_prompt + f\"question: {example['question']}\"\n                            )\n                            message_log.append(self.user_message(initial_prompt))\n                        else:\n                            message_log.append(self.user_message(example[\"question\"]))\n                        message_log.append(self.assistant_message(example[\"sql\"]))\n\n            message_log.append(self.user_message(question))\n        return message_log\n\n    def submit_prompt(self, prompt, **kwargs) -> str:\n        if prompt is None:\n            raise Exception(\"Prompt is None\")\n\n        if len(prompt) == 0:\n            raise Exception(\"Prompt is empty\")\n\n        # Count the number of tokens in the message log\n        # Use 4 as an approximation for the number of characters per token\n        num_tokens = 0\n        for message in prompt:\n            num_tokens += len(message[\"content\"]) / 4\n\n        if kwargs.get(\"model\", None) is not None:\n            model = kwargs.get(\"model\", None)\n            print(f\"Using model {model} for {num_tokens} tokens (approx)\")\n            response = self.client.do(\n                model=self.model,\n                messages=prompt,\n                max_output_tokens=self.max_tokens,\n                stop=None,\n                temperature=self.temperature,\n            )\n        elif self.config is not None and \"model\" in self.config:\n            print(\n                f\"Using model {self.config['model']} for {num_tokens} tokens (approx)\"\n            )\n            response = self.client.do(\n                model=self.config.get(\"model\"),\n                messages=prompt,\n                max_output_tokens=self.max_tokens,\n                stop=None,\n                temperature=self.temperature,\n            )\n        else:\n            if num_tokens > 3500:\n                model = \"ERNIE-Speed-128K\"\n            else:\n                model = \"ERNIE-Speed-8K\"\n\n            print(f\"Using model {model} for {num_tokens} tokens (approx)\")\n            response = self.client.do(\n                model=model,\n                messages=prompt,\n                max_output_tokens=self.max_tokens,\n                stop=None,\n                temperature=self.temperature,\n            )\n\n        return response.body.get(\"result\")\n"
  },
  {
    "path": "src/vanna/legacy/qianfan/Qianfan_embeddings.py",
    "content": "import qianfan\n\nfrom ..base import VannaBase\n\n\nclass Qianfan_Embeddings(VannaBase):\n    def __init__(self, client=None, config=None):\n        VannaBase.__init__(self, config=config)\n\n        if client is not None:\n            self.client = client\n            return\n\n        if \"api_key\" not in config:\n            raise Exception(\"Missing api_key in config\")\n        self.api_key = config[\"api_key\"]\n\n        if \"secret_key\" not in config:\n            raise Exception(\"Missing secret_key in config\")\n        self.secret_key = config[\"secret_key\"]\n\n        self.client = qianfan.Embedding(ak=self.api_key, sk=self.secret_key)\n\n    def generate_embedding(self, data: str, **kwargs) -> list[float]:\n        if self.config is not None and \"model\" in self.config:\n            embedding = self.client.do(\n                model=self.config[\"model\"],\n                input=[data],\n            )\n        else:\n            embedding = self.client.do(\n                model=\"bge-large-zh\",\n                input=[data],\n            )\n\n        return embedding.get(\"data\")[0][\"embedding\"]\n"
  },
  {
    "path": "src/vanna/legacy/qianfan/__init__.py",
    "content": "from .Qianfan_Chat import Qianfan_Chat\nfrom .Qianfan_embeddings import Qianfan_Embeddings\n"
  },
  {
    "path": "src/vanna/legacy/qianwen/QianwenAI_chat.py",
    "content": "import os\n\nfrom openai import OpenAI\n\nfrom ..base import VannaBase\n\n\nclass QianWenAI_Chat(VannaBase):\n    def __init__(self, client=None, config=None):\n        VannaBase.__init__(self, config=config)\n\n        # default parameters - can be overrided using config\n        self.temperature = 0.7\n\n        if \"temperature\" in config:\n            self.temperature = config[\"temperature\"]\n\n        if \"api_type\" in config:\n            raise Exception(\n                \"Passing api_type is now deprecated. Please pass an OpenAI client instead.\"\n            )\n\n        if \"api_base\" in config:\n            raise Exception(\n                \"Passing api_base is now deprecated. Please pass an OpenAI client instead.\"\n            )\n\n        if \"api_version\" in config:\n            raise Exception(\n                \"Passing api_version is now deprecated. Please pass an OpenAI client instead.\"\n            )\n\n        if client is not None:\n            self.client = client\n            return\n\n        if config is None and client is None:\n            self.client = OpenAI(api_key=os.getenv(\"OPENAI_API_KEY\"))\n            return\n\n        if \"api_key\" in config:\n            if \"base_url\" not in config:\n                self.client = OpenAI(\n                    api_key=config[\"api_key\"],\n                    base_url=\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n                )\n            else:\n                self.client = OpenAI(\n                    api_key=config[\"api_key\"], base_url=config[\"base_url\"]\n                )\n\n    def system_message(self, message: str) -> any:\n        return {\"role\": \"system\", \"content\": message}\n\n    def user_message(self, message: str) -> any:\n        return {\"role\": \"user\", \"content\": message}\n\n    def assistant_message(self, message: str) -> any:\n        return {\"role\": \"assistant\", \"content\": message}\n\n    def submit_prompt(self, prompt, **kwargs) -> str:\n        if prompt is None:\n            raise Exception(\"Prompt is None\")\n\n        if len(prompt) == 0:\n            raise Exception(\"Prompt is empty\")\n\n        # Count the number of tokens in the message log\n        # Use 4 as an approximation for the number of characters per token\n        num_tokens = 0\n        for message in prompt:\n            num_tokens += len(message[\"content\"]) / 4\n\n        if kwargs.get(\"model\", None) is not None:\n            model = kwargs.get(\"model\", None)\n            print(f\"Using model {model} for {num_tokens} tokens (approx)\")\n            response = self.client.chat.completions.create(\n                model=model,\n                messages=prompt,\n                stop=None,\n                temperature=self.temperature,\n            )\n        elif kwargs.get(\"engine\", None) is not None:\n            engine = kwargs.get(\"engine\", None)\n            print(f\"Using model {engine} for {num_tokens} tokens (approx)\")\n            response = self.client.chat.completions.create(\n                engine=engine,\n                messages=prompt,\n                stop=None,\n                temperature=self.temperature,\n            )\n        elif self.config is not None and \"engine\" in self.config:\n            print(\n                f\"Using engine {self.config['engine']} for {num_tokens} tokens (approx)\"\n            )\n            response = self.client.chat.completions.create(\n                engine=self.config[\"engine\"],\n                messages=prompt,\n                stop=None,\n                temperature=self.temperature,\n            )\n        elif self.config is not None and \"model\" in self.config:\n            print(\n                f\"Using model {self.config['model']} for {num_tokens} tokens (approx)\"\n            )\n            response = self.client.chat.completions.create(\n                model=self.config[\"model\"],\n                messages=prompt,\n                stop=None,\n                temperature=self.temperature,\n            )\n        else:\n            if num_tokens > 3500:\n                model = \"qwen-long\"\n            else:\n                model = \"qwen-plus\"\n\n            print(f\"Using model {model} for {num_tokens} tokens (approx)\")\n            response = self.client.chat.completions.create(\n                model=model,\n                messages=prompt,\n                stop=None,\n                temperature=self.temperature,\n            )\n\n        # Find the first response from the chatbot that has text in it (some responses may not have text)\n        for choice in response.choices:\n            if \"text\" in choice:\n                return choice.text\n\n        # If no response with text is found, return the first response's content (which may be empty)\n        return response.choices[0].message.content\n"
  },
  {
    "path": "src/vanna/legacy/qianwen/QianwenAI_embeddings.py",
    "content": "from openai import OpenAI\n\nfrom ..base import VannaBase\n\n\nclass QianWenAI_Embeddings(VannaBase):\n    def __init__(self, client=None, config=None):\n        VannaBase.__init__(self, config=config)\n\n        if client is not None:\n            self.client = client\n            return\n\n        if self.client is not None:\n            return\n\n        self.client = OpenAI()\n\n        if config is None:\n            return\n\n        if \"api_type\" in config:\n            self.client.api_type = config[\"api_type\"]\n\n        if \"api_base\" in config:\n            self.client.api_base = config[\"api_base\"]\n\n        if \"api_version\" in config:\n            self.client.api_version = config[\"api_version\"]\n\n        if \"api_key\" in config:\n            self.client.api_key = config[\"api_key\"]\n\n    def generate_embedding(self, data: str, **kwargs) -> list[float]:\n        if self.config is not None and \"engine\" in self.config:\n            embedding = self.client.embeddings.create(\n                engine=self.config[\"engine\"],\n                input=data,\n            )\n        else:\n            embedding = self.client.embeddings.create(\n                model=\"bge-large-zh\",\n                input=data,\n            )\n\n        return embedding.get(\"data\")[0][\"embedding\"]\n"
  },
  {
    "path": "src/vanna/legacy/qianwen/__init__.py",
    "content": "from .QianwenAI_chat import QianWenAI_Chat\nfrom .QianwenAI_embeddings import QianWenAI_Embeddings\n"
  },
  {
    "path": "src/vanna/legacy/remote.py",
    "content": "import dataclasses\nimport json\nfrom io import StringIO\nfrom typing import Callable, List, Tuple, Union\n\nimport pandas as pd\nimport requests\n\nfrom .base import VannaBase\nfrom .types import (\n    AccuracyStats,\n    ApiKey,\n    DataFrameJSON,\n    DataResult,\n    Explanation,\n    FullQuestionDocument,\n    NewOrganization,\n    NewOrganizationMember,\n    Organization,\n    OrganizationList,\n    PlotlyResult,\n    Question,\n    QuestionCategory,\n    QuestionId,\n    QuestionList,\n    QuestionSQLPair,\n    QuestionStringList,\n    SQLAnswer,\n    Status,\n    StatusWithId,\n    StringData,\n    TrainingData,\n    UserEmail,\n    UserOTP,\n    Visibility,\n)\nfrom .vannadb import VannaDB_VectorStore\n\n\nclass VannaDefault(VannaDB_VectorStore):\n    def __init__(self, model: str, api_key: str, config=None):\n        VannaBase.__init__(self, config=config)\n        VannaDB_VectorStore.__init__(\n            self, vanna_model=model, vanna_api_key=api_key, config=config\n        )\n\n        self._model = model\n        self._api_key = api_key\n\n        self._endpoint = (\n            \"https://ask.vanna.ai/rpc\"\n            if config is None or \"endpoint\" not in config\n            else config[\"endpoint\"]\n        )\n\n    def system_message(self, message: str) -> any:\n        return {\"role\": \"system\", \"content\": message}\n\n    def user_message(self, message: str) -> any:\n        return {\"role\": \"user\", \"content\": message}\n\n    def assistant_message(self, message: str) -> any:\n        return {\"role\": \"assistant\", \"content\": message}\n\n    def submit_prompt(self, prompt, **kwargs) -> str:\n        # JSON-ify the prompt\n        json_prompt = json.dumps(prompt, ensure_ascii=False)\n\n        params = [StringData(data=json_prompt)]\n\n        d = self._rpc_call(method=\"submit_prompt\", params=params)\n\n        if \"result\" not in d:\n            return None\n\n        # Load the result into a dataclass\n        results = StringData(**d[\"result\"])\n\n        return results.data\n"
  },
  {
    "path": "src/vanna/legacy/types/__init__.py",
    "content": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import Dict, List, Union\n\n\n@dataclass\nclass Status:\n    success: bool\n    message: str\n\n\n@dataclass\nclass StatusWithId:\n    success: bool\n    message: str\n    id: str\n\n\n@dataclass\nclass QuestionList:\n    questions: List[FullQuestionDocument]\n\n\n@dataclass\nclass FullQuestionDocument:\n    id: QuestionId\n    question: Question\n    answer: SQLAnswer | None\n    data: DataResult | None\n    plotly: PlotlyResult | None\n\n\n@dataclass\nclass QuestionSQLPair:\n    question: str\n    sql: str\n    tag: Union[str, None]\n\n\n@dataclass\nclass Organization:\n    name: str\n    user: str | None\n    connection: Connection | None\n\n\n@dataclass\nclass OrganizationList:\n    organizations: List[str]\n\n\n@dataclass\nclass QuestionStringList:\n    questions: List[str]\n\n\n@dataclass\nclass Visibility:\n    visibility: bool\n\n\n@dataclass\nclass UserEmail:\n    email: str\n\n\n@dataclass\nclass NewOrganization:\n    org_name: str\n    db_type: str\n\n\n@dataclass\nclass NewOrganizationMember:\n    org_name: str\n    email: str\n    is_admin: bool\n\n\n@dataclass\nclass UserOTP:\n    email: str\n    otp: str\n\n\n@dataclass\nclass ApiKey:\n    key: str\n\n\n@dataclass\nclass QuestionId:\n    id: str\n\n\n@dataclass\nclass Question:\n    question: str\n\n\n@dataclass\nclass QuestionCategory:\n    question: str\n    category: str\n\n    NO_SQL_GENERATED = \"No SQL Generated\"\n    SQL_UNABLE_TO_RUN = \"SQL Unable to Run\"\n    BOOTSTRAP_TRAINING_QUERY = \"Bootstrap Training Query\"\n    SQL_RAN = \"SQL Ran Successfully\"\n    FLAGGED_FOR_REVIEW = \"Flagged for Review\"\n    REVIEWED_AND_APPROVED = \"Reviewed and Approved\"\n    REVIEWED_AND_REJECTED = \"Reviewed and Rejected\"\n    REVIEWED_AND_UPDATED = \"Reviewed and Updated\"\n\n\n@dataclass\nclass AccuracyStats:\n    num_questions: int\n    data: Dict[str, int]\n\n\n@dataclass\nclass Followup:\n    followup: str\n\n\n@dataclass\nclass QuestionEmbedding:\n    question: Question\n    embedding: List[float]\n\n\n@dataclass\nclass Connection:\n    # TODO: implement\n    pass\n\n\n@dataclass\nclass SQLAnswer:\n    raw_answer: str\n    prefix: str\n    postfix: str\n    sql: str\n\n\n@dataclass\nclass Explanation:\n    explanation: str\n\n\n@dataclass\nclass DataResult:\n    question: str | None\n    sql: str | None\n    table_markdown: str\n    error: str | None\n    correction_attempts: int\n\n\n@dataclass\nclass PlotlyResult:\n    plotly_code: str\n\n\n@dataclass\nclass WarehouseDefinition:\n    name: str\n    tables: List[TableDefinition]\n\n\n@dataclass\nclass TableDefinition:\n    schema_name: str\n    table_name: str\n    ddl: str | None\n    columns: List[ColumnDefinition]\n\n\n@dataclass\nclass ColumnDefinition:\n    name: str\n    type: str\n    is_primary_key: bool\n    is_foreign_key: bool\n    foreign_key_table: str\n    foreign_key_column: str\n\n\n@dataclass\nclass Diagram:\n    raw: str\n    mermaid_code: str\n\n\n@dataclass\nclass StringData:\n    data: str\n\n\n@dataclass\nclass DataFrameJSON:\n    data: str\n\n\n@dataclass\nclass TrainingData:\n    questions: List[dict]\n    ddl: List[str]\n    documentation: List[str]\n\n\n@dataclass\nclass TrainingPlanItem:\n    item_type: str\n    item_group: str\n    item_name: str\n    item_value: str\n\n    def __str__(self):\n        if self.item_type == self.ITEM_TYPE_SQL:\n            return f\"Train on SQL: {self.item_group} {self.item_name}\"\n        elif self.item_type == self.ITEM_TYPE_DDL:\n            return f\"Train on DDL: {self.item_group} {self.item_name}\"\n        elif self.item_type == self.ITEM_TYPE_IS:\n            return f\"Train on Information Schema: {self.item_group} {self.item_name}\"\n\n    ITEM_TYPE_SQL = \"sql\"\n    ITEM_TYPE_DDL = \"ddl\"\n    ITEM_TYPE_IS = \"is\"\n\n\nclass TrainingPlan:\n    \"\"\"\n    A class representing a training plan. You can see what's in it, and remove items from it that you don't want trained.\n\n    **Example:**\n    ```python\n    plan = vn.get_training_plan()\n\n    plan.get_summary()\n    ```\n\n    \"\"\"\n\n    _plan: List[TrainingPlanItem]\n\n    def __init__(self, plan: List[TrainingPlanItem]):\n        self._plan = plan\n\n    def __str__(self):\n        return \"\\n\".join(self.get_summary())\n\n    def __repr__(self):\n        return self.__str__()\n\n    def get_summary(self) -> List[str]:\n        \"\"\"\n        **Example:**\n        ```python\n        plan = vn.get_training_plan()\n\n        plan.get_summary()\n        ```\n\n        Get a summary of the training plan.\n\n        Returns:\n            List[str]: A list of strings describing the training plan.\n        \"\"\"\n\n        return [f\"{item}\" for item in self._plan]\n\n    def remove_item(self, item: str):\n        \"\"\"\n        **Example:**\n        ```python\n        plan = vn.get_training_plan()\n\n        plan.remove_item(\"Train on SQL: What is the average salary of employees?\")\n        ```\n\n        Remove an item from the training plan.\n\n        Args:\n            item (str): The item to remove.\n        \"\"\"\n        for plan_item in self._plan:\n            if str(plan_item) == item:\n                self._plan.remove(plan_item)\n                break\n"
  },
  {
    "path": "src/vanna/legacy/utils.py",
    "content": "import hashlib\nimport os\nimport re\nimport uuid\nfrom typing import Union\n\nfrom .exceptions import ImproperlyConfigured, ValidationError\n\n\ndef validate_config_path(path):\n    if not os.path.exists(path):\n        raise ImproperlyConfigured(f\"No such configuration file: {path}\")\n\n    if not os.path.isfile(path):\n        raise ImproperlyConfigured(f\"Config should be a file: {path}\")\n\n    if not os.access(path, os.R_OK):\n        raise ImproperlyConfigured(\n            f\"Cannot read the config file. Please grant read privileges: {path}\"\n        )\n\n\ndef sanitize_model_name(model_name):\n    try:\n        model_name = model_name.lower()\n\n        # Replace spaces with a hyphen\n        model_name = model_name.replace(\" \", \"-\")\n\n        if \"-\" in model_name:\n            # remove double hyphones\n            model_name = re.sub(r\"-+\", \"-\", model_name)\n            if \"_\" in model_name:\n                # If name contains both underscores and hyphen replace all underscores with hyphens\n                model_name = re.sub(r\"_\", \"-\", model_name)\n\n        # Remove special characters only allow underscore\n        model_name = re.sub(r\"[^a-zA-Z0-9-_]\", \"\", model_name)\n\n        # Remove hyphen or underscore if any at the last or first\n        if model_name[-1] in (\"-\", \"_\"):\n            model_name = model_name[:-1]\n        if model_name[0] in (\"-\", \"_\"):\n            model_name = model_name[1:]\n\n        return model_name\n    except Exception as e:\n        raise ValidationError(e)\n\n\ndef deterministic_uuid(content: Union[str, bytes]) -> str:\n    \"\"\"Creates deterministic UUID on hash value of string or byte content.\n\n    Args:\n        content: String or byte representation of data.\n\n    Returns:\n        UUID of the content.\n    \"\"\"\n    if isinstance(content, str):\n        content_bytes = content.encode(\"utf-8\")\n    elif isinstance(content, bytes):\n        content_bytes = content\n    else:\n        raise ValueError(f\"Content type {type(content)} not supported !\")\n\n    hash_object = hashlib.sha256(content_bytes)\n    hash_hex = hash_object.hexdigest()\n    namespace = uuid.UUID(\"00000000-0000-0000-0000-000000000000\")\n    content_uuid = str(uuid.uuid5(namespace, hash_hex))\n\n    return content_uuid\n"
  },
  {
    "path": "src/vanna/legacy/vannadb/__init__.py",
    "content": "from .vannadb_vector import VannaDB_VectorStore\n"
  },
  {
    "path": "src/vanna/legacy/vannadb/vannadb_vector.py",
    "content": "import dataclasses\nimport json\nfrom io import StringIO\n\nimport pandas as pd\nimport requests\n\nfrom ..advanced import VannaAdvanced\nfrom ..base import VannaBase\nfrom ..types import (\n    DataFrameJSON,\n    NewOrganization,\n    OrganizationList,\n    Question,\n    QuestionSQLPair,\n    Status,\n    StatusWithId,\n    StringData,\n    TrainingData,\n)\nfrom ..utils import sanitize_model_name\n\n\nclass VannaDB_VectorStore(VannaBase, VannaAdvanced):\n    def __init__(self, vanna_model: str, vanna_api_key: str, config=None):\n        VannaBase.__init__(self, config=config)\n\n        self._model = vanna_model\n        self._api_key = vanna_api_key\n\n        self._endpoint = (\n            \"https://ask.vanna.ai/rpc\"\n            if config is None or \"endpoint\" not in config\n            else config[\"endpoint\"]\n        )\n        self.related_training_data = {}\n        self._graphql_endpoint = \"https://functionrag.com/query\"\n        self._graphql_headers = {\n            \"Content-Type\": \"application/json\",\n            \"API-KEY\": self._api_key,\n            \"NAMESPACE\": self._model,\n        }\n\n    def _rpc_call(self, method, params):\n        if method != \"list_orgs\":\n            headers = {\n                \"Content-Type\": \"application/json\",\n                \"Vanna-Key\": self._api_key,\n                \"Vanna-Org\": self._model,\n            }\n        else:\n            headers = {\n                \"Content-Type\": \"application/json\",\n                \"Vanna-Key\": self._api_key,\n                \"Vanna-Org\": \"demo-tpc-h\",\n            }\n\n        data = {\n            \"method\": method,\n            \"params\": [self._dataclass_to_dict(obj) for obj in params],\n        }\n\n        response = requests.post(self._endpoint, headers=headers, data=json.dumps(data))\n        return response.json()\n\n    def _dataclass_to_dict(self, obj):\n        return dataclasses.asdict(obj)\n\n    def get_all_functions(self) -> list:\n        query = \"\"\"\n            {\n                get_all_sql_functions {\n                    function_name\n                    description\n                    post_processing_code_template\n                    arguments {\n                        name\n                        description\n                        general_type\n                        is_user_editable\n                        available_values\n                    }\n                    sql_template\n                }\n            }\n        \"\"\"\n\n        response = requests.post(\n            self._graphql_endpoint, headers=self._graphql_headers, json={\"query\": query}\n        )\n        response_json = response.json()\n        if (\n            response.status_code == 200\n            and \"data\" in response_json\n            and \"get_all_sql_functions\" in response_json[\"data\"]\n        ):\n            self.log(response_json[\"data\"][\"get_all_sql_functions\"])\n            resp = response_json[\"data\"][\"get_all_sql_functions\"]\n\n            print(resp)\n\n            return resp\n        else:\n            raise Exception(\n                f\"Query failed to run by returning code of {response.status_code}. {response.text}\"\n            )\n\n    def get_function(self, question: str, additional_data: dict = {}) -> dict:\n        query = \"\"\"\n        query GetFunction($question: String!, $staticFunctionArguments: [StaticFunctionArgument]) {\n            get_and_instantiate_function(question: $question, static_function_arguments: $staticFunctionArguments) {\n                ... on SQLFunction {\n                function_name\n                description\n                post_processing_code_template\n                instantiated_post_processing_code\n                arguments {\n                    name\n                    description\n                    general_type\n                    is_user_editable\n                    instantiated_value\n                    available_values\n                }\n                sql_template\n                instantiated_sql\n            }\n            }\n        }\n        \"\"\"\n        static_function_arguments = [\n            {\"name\": key, \"value\": str(value)} for key, value in additional_data.items()\n        ]\n        variables = {\n            \"question\": question,\n            \"staticFunctionArguments\": static_function_arguments,\n        }\n        response = requests.post(\n            self._graphql_endpoint,\n            headers=self._graphql_headers,\n            json={\"query\": query, \"variables\": variables},\n        )\n        response_json = response.json()\n        if (\n            response.status_code == 200\n            and \"data\" in response_json\n            and \"get_and_instantiate_function\" in response_json[\"data\"]\n        ):\n            self.log(response_json[\"data\"][\"get_and_instantiate_function\"])\n            resp = response_json[\"data\"][\"get_and_instantiate_function\"]\n\n            print(resp)\n\n            return resp\n        else:\n            raise Exception(\n                f\"Query failed to run by returning code of {response.status_code}. {response.text}\"\n            )\n\n    def create_function(\n        self, question: str, sql: str, plotly_code: str, **kwargs\n    ) -> dict:\n        query = \"\"\"\n        mutation CreateFunction($question: String!, $sql: String!, $plotly_code: String!) {\n            generate_and_create_sql_function(question: $question, sql: $sql, post_processing_code: $plotly_code) {\n                function_name\n                description\n                arguments {\n                    name\n                    description\n                    general_type\n                    is_user_editable\n                }\n                sql_template\n                post_processing_code_template\n            }\n        }\n        \"\"\"\n        variables = {\"question\": question, \"sql\": sql, \"plotly_code\": plotly_code}\n        response = requests.post(\n            self._graphql_endpoint,\n            headers=self._graphql_headers,\n            json={\"query\": query, \"variables\": variables},\n        )\n        response_json = response.json()\n        if (\n            response.status_code == 200\n            and \"data\" in response_json\n            and response_json[\"data\"] is not None\n            and \"generate_and_create_sql_function\" in response_json[\"data\"]\n        ):\n            resp = response_json[\"data\"][\"generate_and_create_sql_function\"]\n\n            print(resp)\n\n            return resp\n        else:\n            raise Exception(\n                f\"Query failed to run by returning code of {response.status_code}. {response.text}\"\n            )\n\n    def update_function(self, old_function_name: str, updated_function: dict) -> bool:\n        \"\"\"\n        Update an existing SQL function based on the provided parameters.\n\n        Args:\n            old_function_name (str): The current name of the function to be updated.\n            updated_function (dict): A dictionary containing the updated function details. Expected keys:\n                - 'function_name': The new name of the function.\n                - 'description': The new description of the function.\n                - 'arguments': A list of dictionaries describing the function arguments.\n                - 'sql_template': The new SQL template for the function.\n                - 'post_processing_code_template': The new post-processing code template.\n\n        Returns:\n            bool: True if the function was successfully updated, False otherwise.\n        \"\"\"\n        mutation = \"\"\"\n        mutation UpdateSQLFunction($input: SQLFunctionUpdate!) {\n            update_sql_function(input: $input)\n        }\n        \"\"\"\n\n        SQLFunctionUpdate = {\n            \"function_name\",\n            \"description\",\n            \"arguments\",\n            \"sql_template\",\n            \"post_processing_code_template\",\n        }\n\n        # Define the expected keys for each argument in the arguments list\n        ArgumentKeys = {\n            \"name\",\n            \"general_type\",\n            \"description\",\n            \"is_user_editable\",\n            \"available_values\",\n        }\n\n        # Function to validate and transform arguments\n        def validate_arguments(args):\n            return [\n                {key: arg[key] for key in arg if key in ArgumentKeys} for arg in args\n            ]\n\n        # Keep only the keys that conform to the SQLFunctionUpdate GraphQL input type\n        updated_function = {\n            key: value\n            for key, value in updated_function.items()\n            if key in SQLFunctionUpdate\n        }\n\n        # Special handling for 'arguments' to ensure they conform to the spec\n        if \"arguments\" in updated_function:\n            updated_function[\"arguments\"] = validate_arguments(\n                updated_function[\"arguments\"]\n            )\n\n        variables = {\n            \"input\": {\"old_function_name\": old_function_name, **updated_function}\n        }\n\n        print(\"variables\", variables)\n\n        response = requests.post(\n            self._graphql_endpoint,\n            headers=self._graphql_headers,\n            json={\"query\": mutation, \"variables\": variables},\n        )\n        response_json = response.json()\n        if (\n            response.status_code == 200\n            and \"data\" in response_json\n            and response_json[\"data\"] is not None\n            and \"update_sql_function\" in response_json[\"data\"]\n        ):\n            return response_json[\"data\"][\"update_sql_function\"]\n        else:\n            raise Exception(\n                f\"Mutation failed to run by returning code of {response.status_code}. {response.text}\"\n            )\n\n    def delete_function(self, function_name: str) -> bool:\n        mutation = \"\"\"\n        mutation DeleteSQLFunction($function_name: String!) {\n            delete_sql_function(function_name: $function_name)\n        }\n        \"\"\"\n        variables = {\"function_name\": function_name}\n        response = requests.post(\n            self._graphql_endpoint,\n            headers=self._graphql_headers,\n            json={\"query\": mutation, \"variables\": variables},\n        )\n        response_json = response.json()\n        if (\n            response.status_code == 200\n            and \"data\" in response_json\n            and response_json[\"data\"] is not None\n            and \"delete_sql_function\" in response_json[\"data\"]\n        ):\n            return response_json[\"data\"][\"delete_sql_function\"]\n        else:\n            raise Exception(\n                f\"Mutation failed to run by returning code of {response.status_code}. {response.text}\"\n            )\n\n    def create_model(self, model: str, **kwargs) -> bool:\n        \"\"\"\n        **Example:**\n        ```python\n        success = vn.create_model(\"my_model\")\n        ```\n        Create a new model.\n\n        Args:\n            model (str): The name of the model to create.\n\n        Returns:\n            bool: True if the model was created, False otherwise.\n        \"\"\"\n        model = sanitize_model_name(model)\n        params = [NewOrganization(org_name=model, db_type=\"\")]\n\n        d = self._rpc_call(method=\"create_org\", params=params)\n\n        if \"result\" not in d:\n            return False\n\n        status = Status(**d[\"result\"])\n\n        return status.success\n\n    def get_models(self) -> list:\n        \"\"\"\n        **Example:**\n        ```python\n        models = vn.get_models()\n        ```\n\n        List the models that belong to the user.\n\n        Returns:\n            List[str]: A list of model names.\n        \"\"\"\n        d = self._rpc_call(method=\"list_my_models\", params=[])\n\n        if \"result\" not in d:\n            return []\n\n        orgs = OrganizationList(**d[\"result\"])\n\n        return orgs.organizations\n\n    def generate_embedding(self, data: str, **kwargs) -> list[float]:\n        # This is done server-side\n        pass\n\n    def add_question_sql(self, question: str, sql: str, **kwargs) -> str:\n        if \"tag\" in kwargs:\n            tag = kwargs[\"tag\"]\n        else:\n            tag = \"Manually Trained\"\n\n        params = [QuestionSQLPair(question=question, sql=sql, tag=tag)]\n\n        d = self._rpc_call(method=\"add_sql\", params=params)\n\n        if \"result\" not in d:\n            raise Exception(\"Error adding question and SQL pair\", d)\n\n        status = StatusWithId(**d[\"result\"])\n\n        return status.id\n\n    def add_ddl(self, ddl: str, **kwargs) -> str:\n        params = [StringData(data=ddl)]\n\n        d = self._rpc_call(method=\"add_ddl\", params=params)\n\n        if \"result\" not in d:\n            raise Exception(\"Error adding DDL\", d)\n\n        status = StatusWithId(**d[\"result\"])\n\n        return status.id\n\n    def add_documentation(self, documentation: str, **kwargs) -> str:\n        params = [StringData(data=documentation)]\n\n        d = self._rpc_call(method=\"add_documentation\", params=params)\n\n        if \"result\" not in d:\n            raise Exception(\"Error adding documentation\", d)\n\n        status = StatusWithId(**d[\"result\"])\n\n        return status.id\n\n    def get_training_data(self, **kwargs) -> pd.DataFrame:\n        params = []\n\n        d = self._rpc_call(method=\"get_training_data\", params=params)\n\n        if \"result\" not in d:\n            return None\n\n        # Load the result into a dataclass\n        training_data = DataFrameJSON(**d[\"result\"])\n\n        df = pd.read_json(StringIO(training_data.data))\n\n        return df\n\n    def remove_training_data(self, id: str, **kwargs) -> bool:\n        params = [StringData(data=id)]\n\n        d = self._rpc_call(method=\"remove_training_data\", params=params)\n\n        if \"result\" not in d:\n            raise Exception(\"Error removing training data\")\n\n        status = Status(**d[\"result\"])\n\n        if not status.success:\n            raise Exception(f\"Error removing training data: {status.message}\")\n\n        return status.success\n\n    def get_related_training_data_cached(self, question: str) -> TrainingData:\n        params = [Question(question=question)]\n\n        d = self._rpc_call(method=\"get_related_training_data\", params=params)\n\n        if \"result\" not in d:\n            return None\n\n        # Load the result into a dataclass\n        training_data = TrainingData(**d[\"result\"])\n\n        self.related_training_data[question] = training_data\n\n        return training_data\n\n    def get_similar_question_sql(self, question: str, **kwargs) -> list:\n        if question in self.related_training_data:\n            training_data = self.related_training_data[question]\n        else:\n            training_data = self.get_related_training_data_cached(question)\n\n        return training_data.questions\n\n    def get_related_ddl(self, question: str, **kwargs) -> list:\n        if question in self.related_training_data:\n            training_data = self.related_training_data[question]\n        else:\n            training_data = self.get_related_training_data_cached(question)\n\n        return training_data.ddl\n\n    def get_related_documentation(self, question: str, **kwargs) -> list:\n        if question in self.related_training_data:\n            training_data = self.related_training_data[question]\n        else:\n            training_data = self.get_related_training_data_cached(question)\n\n        return training_data.documentation\n"
  },
  {
    "path": "src/vanna/legacy/vllm/__init__.py",
    "content": "from .vllm import Vllm\n"
  },
  {
    "path": "src/vanna/legacy/vllm/vllm.py",
    "content": "import re\n\nimport requests\n\nfrom ..base import VannaBase\n\n\nclass Vllm(VannaBase):\n    def __init__(self, config=None):\n        if config is None or \"vllm_host\" not in config:\n            self.host = \"http://localhost:8000\"\n        else:\n            self.host = config[\"vllm_host\"]\n\n        if config is None or \"model\" not in config:\n            raise ValueError(\"check the config for vllm\")\n        else:\n            self.model = config[\"model\"]\n\n        if \"auth-key\" in config:\n            self.auth_key = config[\"auth-key\"]\n        else:\n            self.auth_key = None\n\n        if \"temperature\" in config:\n            self.temperature = config[\"temperature\"]\n        else:\n            # default temperature - can be overrided using config\n            self.temperature = 0.7\n\n    def system_message(self, message: str) -> any:\n        return {\"role\": \"system\", \"content\": message}\n\n    def user_message(self, message: str) -> any:\n        return {\"role\": \"user\", \"content\": message}\n\n    def assistant_message(self, message: str) -> any:\n        return {\"role\": \"assistant\", \"content\": message}\n\n    def extract_sql_query(self, text):\n        \"\"\"\n        Extracts the first SQL statement after the word 'select', ignoring case,\n        matches until the first semicolon, three backticks, or the end of the string,\n        and removes three backticks if they exist in the extracted string.\n\n        Args:\n        - text (str): The string to search within for an SQL statement.\n\n        Returns:\n        - str: The first SQL statement found, with three backticks removed, or an empty string if no match is found.\n        \"\"\"\n        # Regular expression to find 'select' (ignoring case) and capture until ';', '```', or end of string\n        pattern = re.compile(r\"select.*?(?:;|```|$)\", re.IGNORECASE | re.DOTALL)\n\n        match = pattern.search(text)\n        if match:\n            # Remove three backticks from the matched string if they exist\n            return match.group(0).replace(\"```\", \"\")\n        else:\n            return text\n\n    def generate_sql(self, question: str, **kwargs) -> str:\n        # Use the super generate_sql\n        sql = super().generate_sql(question, **kwargs)\n\n        # Replace \"\\_\" with \"_\"\n        sql = sql.replace(\"\\\\_\", \"_\")\n\n        sql = sql.replace(\"\\\\\", \"\")\n\n        return self.extract_sql_query(sql)\n\n    def submit_prompt(self, prompt, **kwargs) -> str:\n        url = f\"{self.host}/v1/chat/completions\"\n        data = {\n            \"model\": self.model,\n            \"temperature\": self.temperature,\n            \"stream\": False,\n            \"messages\": prompt,\n        }\n\n        if self.auth_key is not None:\n            headers = {\n                \"Content-Type\": \"application/json\",\n                \"Authorization\": f\"Bearer {self.auth_key}\",\n            }\n\n            response = requests.post(url, headers=headers, json=data)\n\n        else:\n            response = requests.post(url, json=data)\n\n        response_dict = response.json()\n\n        self.log(response.text)\n\n        return response_dict[\"choices\"][0][\"message\"][\"content\"]\n"
  },
  {
    "path": "src/vanna/legacy/weaviate/__init__.py",
    "content": "from .weaviate_vector import WeaviateDatabase\n"
  },
  {
    "path": "src/vanna/legacy/weaviate/weaviate_vector.py",
    "content": "import weaviate\nimport weaviate.classes as wvc\nfrom fastembed import TextEmbedding\n\nfrom vanna.base import VannaBase\n\n\nclass WeaviateDatabase(VannaBase):\n    def __init__(self, config=None):\n        \"\"\"\n        Initialize the VannaEnhanced class with the provided configuration.\n\n        :param config: Dictionary containing configuration parameters.\n\n        params:\n        weaviate_url (str): Weaviate cluster URL while using weaviate cloud,\n        weaviate_api_key (str): Weaviate API key while using weaviate cloud,\n        weaviate_port (num): Weaviate port while using local weaviate,\n        weaviate_grpc (num): Weaviate gRPC port while using local weaviate,\n        fastembed_model (str): Fastembed model name for text embeddings. BAAI/bge-small-en-v1.5 by default.\n\n        \"\"\"\n        super().__init__(config=config)\n\n        if config is None:\n            raise ValueError(\"config is required\")\n\n        self.n_results = config.get(\"n_results\", 3)\n        self.fastembed_model = config.get(\"fastembed_model\", \"BAAI/bge-small-en-v1.5\")\n        self.weaviate_api_key = config.get(\"weaviate_api_key\")\n        self.weaviate_url = config.get(\"weaviate_url\")\n        self.weaviate_port = config.get(\"weaviate_port\")\n        self.weaviate_grpc_port = config.get(\"weaviate_grpc\", 50051)\n\n        if not self.weaviate_api_key and not self.weaviate_port:\n            raise ValueError(\"Add proper credentials to connect to weaviate\")\n\n        self.weaviate_client = self._initialize_weaviate_client()\n        self.embeddings = TextEmbedding(model_name=self.fastembed_model)\n\n        self.training_data_cluster = {\n            \"sql\": \"SQLTrainingDataEntry\",\n            \"ddl\": \"DDLEntry\",\n            \"doc\": \"DocumentationEntry\",\n        }\n\n        self._create_collections_if_not_exist()\n\n    def _create_collections_if_not_exist(self):\n        properties_dict = {\n            self.training_data_cluster[\"ddl\"]: [\n                wvc.config.Property(\n                    name=\"description\", data_type=wvc.config.DataType.TEXT\n                ),\n            ],\n            self.training_data_cluster[\"doc\"]: [\n                wvc.config.Property(\n                    name=\"description\", data_type=wvc.config.DataType.TEXT\n                ),\n            ],\n            self.training_data_cluster[\"sql\"]: [\n                wvc.config.Property(name=\"sql\", data_type=wvc.config.DataType.TEXT),\n                wvc.config.Property(\n                    name=\"natural_language_question\", data_type=wvc.config.DataType.TEXT\n                ),\n            ],\n        }\n\n        for cluster, properties in properties_dict.items():\n            if not self.weaviate_client.collections.exists(cluster):\n                self.weaviate_client.collections.create(\n                    name=cluster, properties=properties\n                )\n\n    def _initialize_weaviate_client(self):\n        if self.weaviate_api_key:\n            return weaviate.connect_to_wcs(\n                cluster_url=self.weaviate_url,\n                auth_credentials=weaviate.auth.AuthApiKey(self.weaviate_api_key),\n                additional_config=weaviate.config.AdditionalConfig(timeout=(10, 300)),\n                skip_init_checks=True,\n            )\n        else:\n            return weaviate.connect_to_local(\n                port=self.weaviate_port,\n                grpc_port=self.weaviate_grpc_port,\n                additional_config=weaviate.config.AdditionalConfig(timeout=(10, 300)),\n                skip_init_checks=True,\n            )\n\n    def generate_embedding(self, data: str, **kwargs):\n        embedding_model = TextEmbedding(model_name=self.fastembed_model)\n        embedding = next(embedding_model.embed(data))\n        return embedding.tolist()\n\n    def _insert_data(self, cluster_key: str, data_object: dict, vector: list) -> str:\n        self.weaviate_client.connect()\n        response = self.weaviate_client.collections.get(\n            self.training_data_cluster[cluster_key]\n        ).data.insert(properties=data_object, vector=vector)\n        self.weaviate_client.close()\n        return response\n\n    def add_ddl(self, ddl: str, **kwargs) -> str:\n        data_object = {\n            \"description\": ddl,\n        }\n        response = self._insert_data(\"ddl\", data_object, self.generate_embedding(ddl))\n        return f\"{response}-ddl\"\n\n    def add_documentation(self, doc: str, **kwargs) -> str:\n        data_object = {\n            \"description\": doc,\n        }\n        response = self._insert_data(\"doc\", data_object, self.generate_embedding(doc))\n        return f\"{response}-doc\"\n\n    def add_question_sql(self, question: str, sql: str, **kwargs) -> str:\n        data_object = {\n            \"sql\": sql,\n            \"natural_language_question\": question,\n        }\n        response = self._insert_data(\n            \"sql\", data_object, self.generate_embedding(question)\n        )\n        return f\"{response}-sql\"\n\n    def _query_collection(\n        self, cluster_key: str, vector_input: list, return_properties: list\n    ) -> list:\n        self.weaviate_client.connect()\n        collection = self.weaviate_client.collections.get(\n            self.training_data_cluster[cluster_key]\n        )\n        response = collection.query.near_vector(\n            near_vector=vector_input,\n            limit=self.n_results,\n            return_properties=return_properties,\n        )\n        response_list = [item.properties for item in response.objects]\n        self.weaviate_client.close()\n        return response_list\n\n    def get_related_ddl(self, question: str, **kwargs) -> list:\n        vector_input = self.generate_embedding(question)\n        response_list = self._query_collection(\"ddl\", vector_input, [\"description\"])\n        return [item[\"description\"] for item in response_list]\n\n    def get_related_documentation(self, question: str, **kwargs) -> list:\n        vector_input = self.generate_embedding(question)\n        response_list = self._query_collection(\"doc\", vector_input, [\"description\"])\n        return [item[\"description\"] for item in response_list]\n\n    def get_similar_question_sql(self, question: str, **kwargs) -> list:\n        vector_input = self.generate_embedding(question)\n        response_list = self._query_collection(\n            \"sql\", vector_input, [\"sql\", \"natural_language_question\"]\n        )\n        return [\n            {\"question\": item[\"natural_language_question\"], \"sql\": item[\"sql\"]}\n            for item in response_list\n        ]\n\n    def get_training_data(self, **kwargs) -> list:\n        self.weaviate_client.connect()\n        combined_response_list = []\n        for collection_name in self.training_data_cluster.values():\n            if self.weaviate_client.collections.exists(collection_name):\n                collection = self.weaviate_client.collections.get(collection_name)\n                response_list = [item.properties for item in collection.iterator()]\n                combined_response_list.extend(response_list)\n        self.weaviate_client.close()\n        return combined_response_list\n\n    def remove_training_data(self, id: str, **kwargs) -> bool:\n        self.weaviate_client.connect()\n        success = False\n        if id.endswith(\"-sql\"):\n            id = id.replace(\"-sql\", \"\")\n            success = self.weaviate_client.collections.get(\n                self.training_data_cluster[\"sql\"]\n            ).data.delete_by_id(id)\n        elif id.endswith(\"-ddl\"):\n            id = id.replace(\"-ddl\", \"\")\n            success = self.weaviate_client.collections.get(\n                self.training_data_cluster[\"ddl\"]\n            ).data.delete_by_id(id)\n        elif id.endswith(\"-doc\"):\n            id = id.replace(\"-doc\", \"\")\n            success = self.weaviate_client.collections.get(\n                self.training_data_cluster[\"doc\"]\n            ).data.delete_by_id(id)\n        self.weaviate_client.close()\n        return success\n"
  },
  {
    "path": "src/vanna/legacy/xinference/__init__.py",
    "content": "from .xinference import Xinference\n"
  },
  {
    "path": "src/vanna/legacy/xinference/xinference.py",
    "content": "from xinference_client.client.restful.restful_client import (\n    Client,\n    RESTfulChatModelHandle,\n)\n\nfrom ..base import VannaBase\n\n\nclass Xinference(VannaBase):\n    def __init__(self, config=None):\n        VannaBase.__init__(self, config=config)\n\n        if not config or \"base_url\" not in config:\n            raise ValueError(\"config must contain at least Xinference base_url\")\n\n        base_url = config[\"base_url\"]\n        api_key = config.get(\"api_key\", \"not empty\")\n        self.xinference_client = Client(base_url=base_url, api_key=api_key)\n\n    def system_message(self, message: str) -> any:\n        return {\"role\": \"system\", \"content\": message}\n\n    def user_message(self, message: str) -> any:\n        return {\"role\": \"user\", \"content\": message}\n\n    def assistant_message(self, message: str) -> any:\n        return {\"role\": \"assistant\", \"content\": message}\n\n    def submit_prompt(self, prompt, **kwargs) -> str:\n        if prompt is None:\n            raise Exception(\"Prompt is None\")\n\n        if len(prompt) == 0:\n            raise Exception(\"Prompt is empty\")\n\n        num_tokens = 0\n        for message in prompt:\n            num_tokens += len(message[\"content\"]) / 4\n\n        model_uid = kwargs.get(\"model_uid\") or self.config.get(\"model_uid\", None)\n        if model_uid is None:\n            raise ValueError(\"model_uid is required\")\n\n        xinference_model = self.xinference_client.get_model(model_uid)\n        if isinstance(xinference_model, RESTfulChatModelHandle):\n            print(f\"Using model_uid {model_uid} for {num_tokens} tokens (approx)\")\n\n            response = xinference_model.chat(prompt)\n            return response[\"choices\"][0][\"message\"][\"content\"]\n        else:\n            raise NotImplementedError(\n                f\"Xinference model handle type {type(xinference_model)} is not supported, required RESTfulChatModelHandle\"\n            )\n"
  },
  {
    "path": "src/vanna/py.typed",
    "content": ""
  },
  {
    "path": "src/vanna/servers/__init__.py",
    "content": "\"\"\"\nServer implementations for the Vanna Agents framework.\n\nThis module provides Flask and FastAPI server factories for serving\nVanna agents over HTTP with SSE, WebSocket, and polling endpoints.\n\"\"\"\n\nfrom .base import ChatHandler, ChatRequest, ChatStreamChunk\nfrom .cli.server_runner import ExampleAgentLoader\n\n__all__ = [\n    \"ChatHandler\",\n    \"ChatRequest\",\n    \"ChatStreamChunk\",\n    \"ExampleAgentLoader\",\n]\n"
  },
  {
    "path": "src/vanna/servers/__main__.py",
    "content": "\"\"\"\nEntry point for running Vanna Agents servers.\n\"\"\"\n\nfrom .cli.server_runner import main\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "src/vanna/servers/base/__init__.py",
    "content": "\"\"\"\nBase server components for the Vanna Agents framework.\n\nThis module provides framework-agnostic components for handling chat\nrequests and responses.\n\"\"\"\n\nfrom .chat_handler import ChatHandler\nfrom .models import ChatRequest, ChatStreamChunk, ChatResponse\nfrom .templates import INDEX_HTML\n\n__all__ = [\n    \"ChatHandler\",\n    \"ChatRequest\",\n    \"ChatStreamChunk\",\n    \"ChatResponse\",\n    \"INDEX_HTML\",\n]\n"
  },
  {
    "path": "src/vanna/servers/base/chat_handler.py",
    "content": "\"\"\"\nFramework-agnostic chat handling logic.\n\"\"\"\n\nimport uuid\nfrom typing import AsyncGenerator, List\n\nfrom ...core import Agent\nfrom .models import ChatRequest, ChatResponse, ChatStreamChunk\n\n\nclass ChatHandler:\n    \"\"\"Core chat handling logic - framework agnostic.\"\"\"\n\n    def __init__(\n        self,\n        agent: Agent,\n    ):\n        \"\"\"Initialize chat handler.\n\n        Args:\n            agent: The agent to handle chat requests\n        \"\"\"\n        self.agent = agent\n\n    async def handle_stream(\n        self, request: ChatRequest\n    ) -> AsyncGenerator[ChatStreamChunk, None]:\n        \"\"\"Stream chat responses.\n\n        Args:\n            request: Chat request\n\n        Yields:\n            Chat stream chunks\n        \"\"\"\n        conversation_id = request.conversation_id or self._generate_conversation_id()\n        # Use request_id from client for tracking, or use the one generated internally\n        request_id = request.request_id or str(uuid.uuid4())\n\n        async for component in self.agent.send_message(\n            request_context=request.request_context,\n            message=request.message,\n            conversation_id=conversation_id,\n        ):\n            yield ChatStreamChunk.from_component(component, conversation_id, request_id)\n\n    async def handle_poll(self, request: ChatRequest) -> ChatResponse:\n        \"\"\"Handle polling-based chat.\n\n        Args:\n            request: Chat request\n\n        Returns:\n            Complete chat response\n        \"\"\"\n        chunks = []\n        async for chunk in self.handle_stream(request):\n            chunks.append(chunk)\n\n        return ChatResponse.from_chunks(chunks)\n\n    def _generate_conversation_id(self) -> str:\n        \"\"\"Generate new conversation ID.\"\"\"\n        return f\"conv_{uuid.uuid4().hex[:8]}\"\n"
  },
  {
    "path": "src/vanna/servers/base/models.py",
    "content": "\"\"\"\nRequest and response models for server endpoints.\n\"\"\"\n\nimport time\nimport uuid\nfrom typing import Any, Dict, List, Optional, Union\n\nfrom pydantic import BaseModel, Field\n\nfrom ...components import UiComponent, RichComponent\nfrom ...core.component_manager import ComponentUpdate\nfrom ...core.user.request_context import RequestContext\n\n\nclass ChatRequest(BaseModel):\n    \"\"\"Request model for chat endpoints.\"\"\"\n\n    message: str = Field(description=\"User message\")\n    conversation_id: Optional[str] = Field(default=None, description=\"Conversation ID\")\n    request_id: Optional[str] = Field(\n        default=None, description=\"Request ID for tracing\"\n    )\n    request_context: RequestContext = Field(\n        default_factory=RequestContext,\n        description=\"Request context for user resolution\",\n    )\n    metadata: Dict[str, Any] = Field(\n        default_factory=dict, description=\"Additional metadata\"\n    )\n\n\nclass ChatStreamChunk(BaseModel):\n    \"\"\"Single chunk in a streaming chat response.\"\"\"\n\n    rich: Dict[str, Any] = Field(description=\"Rich component data for advanced UIs\")\n    simple: Optional[Dict[str, Any]] = Field(\n        default=None, description=\"Simple component data for basic UIs\"\n    )\n\n    # Stream metadata\n    conversation_id: str = Field(description=\"Conversation ID\")\n    request_id: str = Field(description=\"Request ID\")\n    timestamp: float = Field(default_factory=time.time, description=\"Timestamp\")\n\n    @classmethod\n    def from_component(\n        cls,\n        component: Union[UiComponent, RichComponent],\n        conversation_id: str,\n        request_id: str,\n    ) -> \"ChatStreamChunk\":\n        \"\"\"Create chunk from UI component or rich component.\"\"\"\n\n        if isinstance(component, UiComponent):\n            # Full UiComponent with both rich and simple\n            rich_data = component.rich_component.serialize_for_frontend()\n            simple_data = None\n            if component.simple_component:\n                simple_data = component.simple_component.serialize_for_frontend()\n\n            return cls(\n                rich=rich_data,\n                simple=simple_data,\n                conversation_id=conversation_id,\n                request_id=request_id,\n            )\n\n        # Rich component only (no simple fallback)\n        rich_data = component.serialize_for_frontend()\n        return cls(\n            rich=rich_data,\n            simple=None,\n            conversation_id=conversation_id,\n            request_id=request_id,\n        )\n\n    @classmethod\n    def from_component_update(\n        cls, update: ComponentUpdate, conversation_id: str, request_id: str\n    ) -> \"ChatStreamChunk\":\n        \"\"\"Create chunk from component update.\"\"\"\n        update_payload = update.serialize_for_frontend()\n        return cls(\n            rich=update_payload,\n            simple=None,  # Component updates don't have simple representations\n            conversation_id=conversation_id,\n            request_id=request_id,\n        )\n\n\nclass ChatResponse(BaseModel):\n    \"\"\"Complete chat response for polling endpoints.\"\"\"\n\n    chunks: List[ChatStreamChunk] = Field(description=\"Response chunks\")\n    conversation_id: str = Field(description=\"Conversation ID\")\n    request_id: str = Field(description=\"Request ID\")\n    total_chunks: int = Field(description=\"Total number of chunks\")\n\n    @classmethod\n    def from_chunks(cls, chunks: List[ChatStreamChunk]) -> \"ChatResponse\":\n        \"\"\"Create response from chunks.\"\"\"\n        if not chunks:\n            return cls(chunks=[], conversation_id=\"\", request_id=\"\", total_chunks=0)\n\n        return cls(\n            chunks=chunks,\n            conversation_id=chunks[0].conversation_id,\n            request_id=chunks[0].request_id,\n            total_chunks=len(chunks),\n        )\n"
  },
  {
    "path": "src/vanna/servers/base/rich_chat_handler.py",
    "content": "# \"\"\"\n# Rich component-aware chat handling logic.\n# \"\"\"\n\n# import uuid\n# from typing import AsyncGenerator, Callable, List, Optional, Union\n\n# from ...core import Agent, User\n# from ...core.rich_components import RichComponent\n# from ...core.component_manager import ComponentManager, ComponentUpdate\n# from .models import ChatRequest, ChatResponse, ChatStreamChunk\n\n\n# class RichChatHandler:\n#     \"\"\"Rich component-aware chat handling logic.\"\"\"\n\n#     def __init__(\n#         self,\n#         agent: Agent,\n#         default_user_factory: Optional[Callable[[Optional[str]], User]] = None,\n#     ):\n#         \"\"\"Initialize rich chat handler.\n\n#         Args:\n#             agent: The agent to handle chat requests\n#             default_user_factory: Function to create default user from user_id\n#         \"\"\"\n#         self.agent = agent\n#         self.default_user_factory = default_user_factory or self._create_default_user\n#         self.component_managers: dict[str, ComponentManager] = {}  # Per conversation\n\n#     async def handle_stream(\n#         self, request: ChatRequest\n#     ) -> AsyncGenerator[ChatStreamChunk, None]:\n#         \"\"\"Stream chat responses with rich component support.\n\n#         Args:\n#             request: Chat request\n\n#         Yields:\n#             Chat stream chunks including rich component updates\n#         \"\"\"\n#         user = self._resolve_user(request.user_id)\n#         conversation_id = request.conversation_id or self._generate_conversation_id()\n#         request_id = request.request_id or str(uuid.uuid4())\n\n#         # Get or create component manager for this conversation\n#         if conversation_id not in self.component_managers:\n#             self.component_managers[conversation_id] = ComponentManager()\n\n#         component_manager = self.component_managers[conversation_id]\n\n#         async for component in self.agent.send_message(\n#             conversation_id=conversation_id,\n#             user=user,\n#             message=request.message,\n#             request_id=request_id,\n#         ):\n#             if isinstance(component, RichComponent):\n#                 # Handle rich component through manager\n#                 update = component_manager.emit(component)\n#                 yield ChatStreamChunk.from_component_update(update, conversation_id, request_id)\n#             else:\n#                 # Handle legacy components\n#                 yield ChatStreamChunk.from_component(component, conversation_id, request_id)\n\n#     async def handle_poll(self, request: ChatRequest) -> ChatResponse:\n#         \"\"\"Handle polling request with rich component support.\n\n#         Args:\n#             request: Chat request\n\n#         Returns:\n#             Complete chat response with all components\n#         \"\"\"\n#         chunks: List[ChatStreamChunk] = []\n\n#         async for chunk in self.handle_stream(request):\n#             chunks.append(chunk)\n\n#         return ChatResponse.from_chunks(chunks)\n\n#     def get_component_manager(self, conversation_id: str) -> Optional[ComponentManager]:\n#         \"\"\"Get the component manager for a conversation.\"\"\"\n#         return self.component_managers.get(conversation_id)\n\n#     def get_component(self, conversation_id: str, component_id: str) -> Optional[RichComponent]:\n#         \"\"\"Get a specific component from a conversation.\"\"\"\n#         manager = self.get_component_manager(conversation_id)\n#         return manager.get_component(component_id) if manager else None\n\n#     def get_all_components(self, conversation_id: str) -> List[RichComponent]:\n#         \"\"\"Get all components in a conversation.\"\"\"\n#         manager = self.get_component_manager(conversation_id)\n#         return manager.get_all_components() if manager else []\n\n#     def update_component(\n#         self,\n#         conversation_id: str,\n#         component_id: str,\n#         **updates\n#     ) -> Optional[ComponentUpdate]:\n#         \"\"\"Update a component in a conversation.\"\"\"\n#         manager = self.get_component_manager(conversation_id)\n#         return manager.update_component(component_id, **updates) if manager else None\n\n#     def remove_component(\n#         self,\n#         conversation_id: str,\n#         component_id: str\n#     ) -> Optional[ComponentUpdate]:\n#         \"\"\"Remove a component from a conversation.\"\"\"\n#         manager = self.get_component_manager(conversation_id)\n#         return manager.remove_component(component_id) if manager else None\n\n#     def clear_conversation_components(self, conversation_id: str):\n#         \"\"\"Clear all components for a conversation.\"\"\"\n#         if conversation_id in self.component_managers:\n#             del self.component_managers[conversation_id]\n\n#     def _resolve_user(self, user_id: Optional[str]) -> User:\n#         \"\"\"Resolve user from ID or create default.\"\"\"\n#         if user_id:\n#             # In a real implementation, you'd fetch from a user store\n#             return User(id=user_id, username=f\"user_{user_id}\", email=\"\", permissions=[])\n\n#         return self.default_user_factory(user_id)\n\n#     def _create_default_user(self, user_id: Optional[str]) -> User:\n#         \"\"\"Create a default user.\"\"\"\n#         user_id = user_id or \"anonymous\"\n#         return User(\n#             id=user_id,\n#             username=f\"user_{user_id}\",\n#             email=\"\",\n#             permissions=[]\n#         )\n\n#     def _generate_conversation_id(self) -> str:\n#         \"\"\"Generate a new conversation ID.\"\"\"\n#         return str(uuid.uuid4())\n"
  },
  {
    "path": "src/vanna/servers/base/templates.py",
    "content": "\"\"\"\nHTML templates for Vanna Agents servers.\n\"\"\"\n\nfrom typing import Optional\n\n\ndef get_vanna_component_script(\n    dev_mode: bool = False,\n    static_path: str = \"/static\",\n    cdn_url: str = \"https://img.vanna.ai/vanna-components.js\",\n) -> str:\n    \"\"\"Get the script tag for loading Vanna web components.\n\n    Args:\n        dev_mode: If True, load from local static files\n        static_path: Path to static assets in dev mode\n        cdn_url: CDN URL for production\n\n    Returns:\n        HTML script tag for loading components\n    \"\"\"\n    if dev_mode:\n        return (\n            f'<script type=\"module\" src=\"{static_path}/vanna-components.js\"></script>'\n        )\n    else:\n        return f'<script type=\"module\" src=\"{cdn_url}\"></script>'\n\n\ndef get_index_html(\n    dev_mode: bool = False,\n    static_path: str = \"/static\",\n    cdn_url: str = \"https://img.vanna.ai/vanna-components.js\",\n    api_base_url: str = \"\",\n) -> str:\n    \"\"\"Generate index HTML with configurable component loading.\n\n    Args:\n        dev_mode: If True, load components from local static files\n        static_path: Path to static assets in dev mode\n        cdn_url: CDN URL for production components\n        api_base_url: Base URL for API endpoints\n\n    Returns:\n        Complete HTML page as string\n    \"\"\"\n    component_script = get_vanna_component_script(dev_mode, static_path, cdn_url)\n\n    return f\"\"\"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Vanna Agents Chat</title>\n    <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n    <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n    <link href=\"https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@400;500;600;700&family=Space+Grotesk:wght@300;400;500;600;700&family=Space+Mono:wght@400;700&display=swap\" rel=\"stylesheet\">\n    <script src=\"https://cdn.tailwindcss.com\"></script>\n    <script>\n        tailwind.config = {{\n            theme: {{\n                extend: {{\n                    colors: {{\n                        'vanna-navy': '#023d60',\n                        'vanna-cream': '#e7e1cf',\n                        'vanna-teal': '#15a8a8',\n                        'vanna-orange': '#fe5d26',\n                        'vanna-magenta': '#bf1363',\n                    }},\n                    fontFamily: {{\n                        'sans': ['Space Grotesk', 'ui-sans-serif', 'system-ui'],\n                        'serif': ['Roboto Slab', 'ui-serif', 'Georgia'],\n                        'mono': ['Space Mono', 'ui-monospace', 'monospace'],\n                    }}\n                }}\n            }}\n        }}\n    </script>\n    <style>\n        body {{\n            background: linear-gradient(to bottom, #e7e1cf, #ffffff, #e7e1cf);\n            min-height: 100vh;\n            position: relative;\n            overflow-x: hidden;\n        }}\n\n        /* Background decorations matching landing page */\n        body::before {{\n            content: '';\n            position: fixed;\n            inset: 0;\n            pointer-events: none;\n            z-index: 0;\n            /* Radial gradients with brand colors */\n            background:\n                radial-gradient(circle at top left, rgba(21, 168, 168, 0.12), transparent 60%),\n                radial-gradient(circle at bottom right, rgba(254, 93, 38, 0.08), transparent 65%);\n        }}\n\n        body::after {{\n            content: '';\n            position: fixed;\n            inset: 0;\n            pointer-events: none;\n            z-index: 0;\n            /* Dot pattern with retro computing aesthetic */\n            background-image: radial-gradient(circle at 2px 2px, rgba(2, 61, 96, 0.3) 1px, transparent 0);\n            background-size: 32px 32px;\n            /* Grid overlay */\n            background-image:\n                radial-gradient(circle at 2px 2px, rgba(2, 61, 96, 0.3) 1px, transparent 0),\n                linear-gradient(rgba(2, 61, 96, 0.1) 1px, transparent 1px),\n                linear-gradient(90deg, rgba(2, 61, 96, 0.1) 1px, transparent 1px);\n            background-size: 32px 32px, 100px 100px, 100px 100px;\n        }}\n\n        /* Ensure content is above background */\n        body > * {{\n            position: relative;\n            z-index: 1;\n        }}\n\n        vanna-chat {{\n            width: 100%;\n            height: 100%;\n            display: block;\n        }}\n    </style>\n    {component_script}\n</head>\n<body>\n    <div class=\"max-w-6xl mx-auto p-5\">\n        <!-- Header -->\n        <div class=\"text-center mb-8\">\n            <h1 class=\"text-4xl font-bold text-vanna-navy mb-2 font-serif\">Vanna Agents</h1>\n            <p class=\"text-lg font-mono font-bold text-vanna-teal mb-4\">DATA-FIRST AGENTS</p>\n            <p class=\"text-slate-600 mb-4\">Interactive AI Assistant powered by Vanna Agents Framework</p>\n            <a href=\"javascript:window.location='view-source:'+window.location.href\" class=\"inline-flex items-center gap-2 px-4 py-2 bg-vanna-teal text-white text-sm font-medium rounded-lg hover:bg-vanna-navy transition\">\n                <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n                    <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4\"/>\n                </svg>\n                View Page Source\n            </a>\n        </div>\n\n        {('    <div class=\"bg-vanna-orange/10 border border-vanna-orange/30 rounded-lg p-3 mb-5 text-vanna-orange text-sm font-medium\">📦 Development Mode: Loading components from local assets</div>' if dev_mode else \"\")}\n\n        <!-- Login Form -->\n        <div id=\"loginContainer\" class=\"max-w-md mx-auto mb-10 bg-white p-8 rounded-xl shadow-lg border border-vanna-teal/30\">\n            <div class=\"text-center mb-6\">\n                <h2 class=\"text-2xl font-semibold text-vanna-navy mb-2 font-serif\">Login to Continue</h2>\n                <p class=\"text-sm text-slate-600\">Select your email to access the chat</p>\n            </div>\n\n            <div class=\"mb-5\">\n                <label for=\"emailInput\" class=\"block mb-2 text-sm font-medium text-vanna-navy\">Email Address</label>\n                <select\n                    id=\"emailInput\"\n                    class=\"w-full px-4 py-3 text-sm border border-vanna-teal/30 rounded-lg focus:outline-none focus:ring-2 focus:ring-vanna-teal focus:border-transparent bg-white\"\n                >\n                    <option value=\"\">Select an email...</option>\n                    <option value=\"admin@example.com\">admin@example.com</option>\n                    <option value=\"user@example.com\">user@example.com</option>\n                </select>\n            </div>\n\n            <button id=\"loginButton\" class=\"w-full px-4 py-3 bg-vanna-teal text-white text-sm font-medium rounded-lg hover:bg-vanna-navy focus:outline-none focus:ring-2 focus:ring-vanna-teal focus:ring-offset-2 transition disabled:bg-gray-400 disabled:cursor-not-allowed\">\n                Continue\n            </button>\n\n            <div class=\"mt-5 p-3 bg-vanna-teal/10 border-l-4 border-vanna-teal rounded text-xs text-vanna-navy leading-relaxed\">\n                <strong>Demo Mode:</strong> This is a frontend-only authentication demo.\n                Your email will be stored as a cookie and automatically sent with all API requests.\n            </div>\n        </div>\n\n        <!-- Logged In Status (hidden by default) -->\n        <div id=\"loggedInStatus\" class=\"hidden text-center p-4 bg-vanna-teal/10 border border-vanna-teal/30 rounded-lg mb-5\">\n            Logged in as <span id=\"loggedInEmail\" class=\"font-semibold text-vanna-navy\"></span>\n            <br>\n            <button id=\"logoutButton\" class=\"mt-2 px-3 py-1.5 bg-vanna-navy text-white text-xs rounded hover:bg-vanna-teal transition\">\n                Logout\n            </button>\n        </div>\n\n        <!-- Chat Container (hidden by default) -->\n        <div id=\"chatSections\" class=\"hidden\">\n            <div class=\"bg-white rounded-xl shadow-lg h-[600px] overflow-hidden border border-vanna-teal/30\">\n                <vanna-chat\n                    api-base=\"{api_base_url}\"\n                    sse-endpoint=\"{api_base_url}/api/vanna/v2/chat_sse\"\n                    ws-endpoint=\"{api_base_url}/api/vanna/v2/chat_websocket\"\n                    poll-endpoint=\"{api_base_url}/api/vanna/v2/chat_poll\">\n                </vanna-chat>\n            </div>\n\n            <div class=\"mt-8 p-5 bg-white rounded-lg shadow border border-vanna-teal/30\">\n                <h3 class=\"text-lg font-semibold text-vanna-navy mb-3 font-serif\">API Endpoints</h3>\n                <ul class=\"space-y-2\">\n                    <li class=\"p-2 bg-vanna-cream/50 rounded font-mono text-sm\">\n                        <span class=\"font-bold text-vanna-teal mr-2\">POST</span>{api_base_url}/api/vanna/v2/chat_sse - Server-Sent Events streaming\n                    </li>\n                    <li class=\"p-2 bg-vanna-cream/50 rounded font-mono text-sm\">\n                        <span class=\"font-bold text-vanna-teal mr-2\">WS</span>{api_base_url}/api/vanna/v2/chat_websocket - WebSocket real-time chat\n                    </li>\n                    <li class=\"p-2 bg-vanna-cream/50 rounded font-mono text-sm\">\n                        <span class=\"font-bold text-vanna-teal mr-2\">POST</span>{api_base_url}/api/vanna/v2/chat_poll - Request/response polling\n                    </li>\n                    <li class=\"p-2 bg-vanna-cream/50 rounded font-mono text-sm\">\n                        <span class=\"font-bold text-vanna-teal mr-2\">GET</span>{api_base_url}/health - Health check\n                    </li>\n                </ul>\n            </div>\n        </div>\n    </div>\n\n    <script>\n        // Cookie helpers\n        const getCookie = (name) => {{\n            const value = `; ${{document.cookie}}`;\n            const parts = value.split(`; ${{name}}=`);\n            return parts.length === 2 ? parts.pop().split(';').shift() : null;\n        }};\n\n        const setCookie = (name, value) => {{\n            const expires = new Date(Date.now() + 365 * 864e5).toUTCString();\n            document.cookie = `${{name}}=${{value}}; expires=${{expires}}; path=/; SameSite=Lax`;\n        }};\n\n        const deleteCookie = (name) => {{\n            document.cookie = `${{name}}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;\n        }};\n\n        // Login/Logout\n        document.addEventListener('DOMContentLoaded', () => {{\n            const email = getCookie('vanna_email');\n\n            // Check if already logged in\n            if (email) {{\n                loginContainer.classList.add('hidden');\n                loggedInStatus.classList.remove('hidden');\n                chatSections.classList.remove('hidden');\n                loggedInEmail.textContent = email;\n            }}\n\n            // Login button\n            loginButton.addEventListener('click', () => {{\n                const email = emailInput.value.trim();\n                if (!email) {{\n                    alert('Please select an email address');\n                    return;\n                }}\n                setCookie('vanna_email', email);\n                loginContainer.classList.add('hidden');\n                loggedInStatus.classList.remove('hidden');\n                chatSections.classList.remove('hidden');\n                loggedInEmail.textContent = email;\n            }});\n\n            // Logout button\n            logoutButton.addEventListener('click', () => {{\n                deleteCookie('vanna_email');\n                loginContainer.classList.remove('hidden');\n                loggedInStatus.classList.add('hidden');\n                chatSections.classList.add('hidden');\n                emailInput.value = '';\n            }});\n\n            // Enter key\n            emailInput.addEventListener('keypress', (e) => {{\n                if (e.key === 'Enter') loginButton.click();\n            }});\n        }});\n    </script>\n\n    <script>\n        // Artifact demo event listener\n        document.addEventListener('DOMContentLoaded', () => {{\n            const vannaChat = document.querySelector('vanna-chat');\n\n            if (vannaChat) {{\n                // Add artifact event listener to demonstrate external rendering\n                vannaChat.addEventListener('artifact-opened', (event) => {{\n                    const {{ artifactId, type, title, trigger }} = event.detail;\n\n                    console.log('🎨 Artifact Event:', {{ artifactId, type, title, trigger }});\n\n                    // For demo: open all artifacts externally\n                    setTimeout(() => {{\n                        const newWindow = window.open('', '_blank', 'width=900,height=700');\n                        if (newWindow) {{\n                            newWindow.document.write(event.detail.getStandaloneHTML());\n                            newWindow.document.close();\n                            newWindow.document.title = title || 'Vanna Artifact';\n                            console.log(`📱 Opened ${{title}} in new window`);\n                        }}\n                    }}, 100);\n\n                    // Prevent default in-chat rendering\n                    event.detail.preventDefault();\n                    console.log('✋ Showing placeholder in chat instead of full artifact');\n                }});\n\n                console.log('🎯 Artifact demo mode: All artifacts will open externally');\n            }}\n        }});\n\n        // Fallback if web component doesn't load\n        if (!customElements.get('vanna-chat')) {{\n            setTimeout(() => {{\n                if (!customElements.get('vanna-chat')) {{\n                    document.querySelector('vanna-chat').innerHTML = `\n                        <div class=\"p-10 text-center text-gray-600\">\n                            <h3 class=\"text-xl font-semibold mb-2\">Vanna Chat Component</h3>\n                            <p class=\"mb-2\">Web component failed to load. Please check your connection.</p>\n                            <p class=\"text-sm text-gray-400\">\n                                {(\"Loading from: local static assets\" if dev_mode else f\"Loading from: {cdn_url}\")}\n                            </p>\n                        </div>\n                    `;\n                }}\n            }}, 2000);\n        }}\n    </script>\n</body>\n</html>\"\"\"\n\n\n# Backward compatibility - default production HTML\nINDEX_HTML = get_index_html()\n"
  },
  {
    "path": "src/vanna/servers/cli/__init__.py",
    "content": "\"\"\"\nCLI components for Vanna Agents servers.\n\"\"\"\n\nfrom .server_runner import ExampleAgentLoader\n\n__all__ = [\"ExampleAgentLoader\"]\n"
  },
  {
    "path": "src/vanna/servers/cli/server_runner.py",
    "content": "\"\"\"\nCLI for running Vanna Agents servers with example agents.\n\"\"\"\n\nimport importlib\nimport json\nfrom typing import Dict, Optional, Any, cast, TextIO, Union\n\nimport click\n\nfrom ...core import Agent\n\n\nclass ExampleAgentLoader:\n    \"\"\"Loads example agents for the CLI.\"\"\"\n\n    @staticmethod\n    def list_available_examples() -> Dict[str, str]:\n        \"\"\"Return available examples with descriptions.\"\"\"\n        return {\n            \"mock_quickstart\": \"Basic agent with mock LLM service\",\n            \"anthropic_quickstart\": \"Agent configured for Anthropic's Claude API\",\n            \"openai_quickstart\": \"Agent configured for OpenAI's GPT models\",\n            \"mock_custom_tool\": \"Agent with custom tool demonstration (mock LLM)\",\n            \"mock_quota_example\": \"Agent with usage quota management (mock LLM)\",\n            \"mock_rich_components_demo\": \"Rich components demonstration with cards, tasks, and progress (mock LLM)\",\n            \"coding_agent_example\": \"Coding agent with file system tools (list, read, write files)\",\n            \"email_auth_example\": \"Email-based authentication demonstration (mock LLM)\",\n            \"claude_sqlite_example\": \"Claude agent with SQLite database querying capabilities\",\n            \"mock_sqlite_example\": \"Mock agent with SQLite database demonstration\",\n        }\n\n    @staticmethod\n    def load_example_agent(example_name: str) -> Agent:\n        \"\"\"Load an example agent by name.\n\n        Args:\n            example_name: Name of the example to load\n\n        Returns:\n            Configured agent instance\n\n        Raises:\n            ValueError: If example not found or failed to load\n        \"\"\"\n        try:\n            # Import the example module\n            module = importlib.import_module(f\"vanna.examples.{example_name}\")\n\n            # Look for standard factory functions\n            factory_functions = [\n                \"create_demo_agent\",\n                \"create_agent\",\n                \"create_basic_demo\",\n            ]\n\n            for func_name in factory_functions:\n                if hasattr(module, func_name):\n                    factory = getattr(module, func_name)\n                    return cast(Agent, factory())\n\n            # Look for module-level agent instances\n            if hasattr(module, \"main_agent\"):\n                return cast(Agent, module.main_agent)\n\n            raise AttributeError(f\"No agent factory found in {example_name}\")\n\n        except ImportError as e:\n            raise ValueError(f\"Example '{example_name}' not found: {e}\")\n        except Exception as e:\n            raise ValueError(f\"Failed to load example '{example_name}': {e}\")\n\n\n@click.command()\n@click.option(\n    \"--framework\",\n    type=click.Choice([\"flask\", \"fastapi\"]),\n    default=\"fastapi\",\n    help=\"Web framework to use\",\n)\n@click.option(\"--port\", default=8000, help=\"Port to run server on\")\n@click.option(\"--host\", default=\"0.0.0.0\", help=\"Host to bind server to\")\n@click.option(\n    \"--example\", help=\"Example agent to use (use --list-examples to see options)\"\n)\n@click.option(\"--list-examples\", is_flag=True, help=\"List available example agents\")\n@click.option(\n    \"--config\", type=click.File(\"r\"), help=\"JSON config file for server settings\"\n)\n@click.option(\"--debug\", is_flag=True, help=\"Enable debug mode\")\n@click.option(\n    \"--dev\",\n    is_flag=True,\n    help=\"Enable development mode (load components from local assets)\",\n)\n@click.option(\n    \"--static-folder\", default=None, help=\"Static folder path for development mode\"\n)\n@click.option(\n    \"--cdn-url\",\n    default=\"https://img.vanna.ai/vanna-components.js\",\n    help=\"CDN URL for web components\",\n)\ndef main(\n    framework: str,\n    port: int,\n    host: str,\n    example: Optional[str],\n    list_examples: bool,\n    config: Optional[click.File],\n    debug: bool,\n    dev: bool,\n    static_folder: Optional[str],\n    cdn_url: str,\n) -> None:\n    \"\"\"Run Vanna Agents server with optional example agent.\"\"\"\n\n    if list_examples:\n        click.echo(\"Available example agents:\")\n        examples = ExampleAgentLoader.list_available_examples()\n        for name, description in examples.items():\n            click.echo(f\"  {name:20} - {description}\")\n        return\n\n    # Load configuration\n    server_config = {}\n    if config:\n        server_config = json.load(cast(TextIO, config))\n\n    # Set default static folder based on dev mode\n    if static_folder is None:\n        static_folder = \"frontend/webcomponent/static\" if dev else \"static\"\n\n    # Add CLI options to config\n    server_config.update(\n        {\n            \"dev_mode\": dev,\n            \"static_folder\": static_folder,\n            \"cdn_url\": cdn_url,\n            \"api_base_url\": \"\",  # Can be overridden in config file\n        }\n    )\n\n    # Create agent\n    if example:\n        try:\n            agent = ExampleAgentLoader.load_example_agent(example)\n            click.echo(f\"✓ Loaded example agent: {example}\")\n        except ValueError as e:\n            click.echo(f\"Error: {e}\", err=True)\n            return\n    else:\n        # Fallback to basic agent\n        try:\n            from ...agents import create_basic_agent\n            from ...integrations.mock import MockLlmService\n\n            llm_service = MockLlmService(\n                response_content=\"Hello! I'm a Vanna Agents demo server. How can I help you?\"\n            )\n            agent = create_basic_agent(llm_service)\n            click.echo(\n                \"✓ Using basic demo agent (use --example to specify different agent)\"\n            )\n        except ImportError as e:\n            click.echo(f\"Error: Could not create basic agent: {e}\", err=True)\n            return\n\n    from ..flask.app import VannaFlaskServer\n    from ..fastapi.app import VannaFastAPIServer\n\n    # Create and run server\n    server: Union[VannaFlaskServer, VannaFastAPIServer]\n    if framework == \"flask\":\n        server = VannaFlaskServer(agent, config=server_config)\n        click.echo(f\"🚀 Starting Flask server on http://{host}:{port}\")\n        if dev:\n            click.echo(\n                f\"📦 Development mode: loading web components from ./{static_folder}/\"\n            )\n        else:\n            click.echo(f\"🌍 Production mode: loading web components from CDN\")\n        try:\n            server.run(host=host, port=port, debug=debug)\n        except KeyboardInterrupt:\n            click.echo(\"\\n👋 Server stopped\")\n    else:\n        server = VannaFastAPIServer(agent, config=server_config)\n        click.echo(f\"🚀 Starting FastAPI server on http://{host}:{port}\")\n        click.echo(f\"📖 API docs available at http://{host}:{port}/docs\")\n        if dev:\n            click.echo(\n                f\"📦 Development mode: loading web components from ./{static_folder}/\"\n            )\n        else:\n            click.echo(f\"🌍 Production mode: loading web components from CDN\")\n        try:\n            server.run(host=host, port=port)\n        except KeyboardInterrupt:\n            click.echo(\"\\n👋 Server stopped\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "src/vanna/servers/fastapi/__init__.py",
    "content": "\"\"\"\nFastAPI server implementation for Vanna Agents.\n\"\"\"\n\nfrom .app import VannaFastAPIServer\n\n__all__ = [\"VannaFastAPIServer\"]\n"
  },
  {
    "path": "src/vanna/servers/fastapi/app.py",
    "content": "\"\"\"\nFastAPI server factory for Vanna Agents.\n\"\"\"\n\nfrom typing import Any, Dict, Optional\n\nfrom fastapi import FastAPI\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom fastapi.staticfiles import StaticFiles\n\nfrom ...core import Agent\nfrom ..base import ChatHandler\nfrom .routes import register_chat_routes\n\n\nclass VannaFastAPIServer:\n    \"\"\"FastAPI server factory for Vanna Agents.\"\"\"\n\n    def __init__(self, agent: Agent, config: Optional[Dict[str, Any]] = None):\n        \"\"\"Initialize FastAPI server.\n\n        Args:\n            agent: The agent to serve (must have user_resolver configured)\n            config: Optional server configuration\n        \"\"\"\n        self.agent = agent\n        self.config = config or {}\n        self.chat_handler = ChatHandler(agent)\n\n    def create_app(self) -> FastAPI:\n        \"\"\"Create configured FastAPI app.\n\n        Returns:\n            Configured FastAPI application\n        \"\"\"\n        # Create FastAPI app\n        app_config = self.config.get(\"fastapi\", {})\n        app = FastAPI(\n            title=\"Vanna Agents API\",\n            description=\"API server for Vanna Agents framework\",\n            version=\"0.1.0\",\n            **app_config,\n        )\n\n        # Configure CORS if enabled\n        cors_config = self.config.get(\"cors\", {})\n        if cors_config.get(\"enabled\", True):\n            cors_params = {k: v for k, v in cors_config.items() if k != \"enabled\"}\n\n            # Set sensible defaults\n            cors_params.setdefault(\"allow_origins\", [\"*\"])\n            cors_params.setdefault(\"allow_credentials\", True)\n            cors_params.setdefault(\"allow_methods\", [\"*\"])\n            cors_params.setdefault(\"allow_headers\", [\"*\"])\n\n            app.add_middleware(CORSMiddleware, **cors_params)\n\n        # Add static file serving in dev mode\n        dev_mode = self.config.get(\"dev_mode\", False)\n        if dev_mode:\n            static_folder = self.config.get(\"static_folder\", \"static\")\n            try:\n                import os\n\n                if os.path.exists(static_folder):\n                    app.mount(\n                        \"/static\", StaticFiles(directory=static_folder), name=\"static\"\n                    )\n            except Exception:\n                pass  # Static files not available\n\n        # Register routes\n        register_chat_routes(app, self.chat_handler, self.config)\n\n        # Add health check\n        @app.get(\"/health\")\n        async def health_check() -> Dict[str, str]:\n            return {\"status\": \"healthy\", \"service\": \"vanna\"}\n\n        return app\n\n    def run(self, **kwargs: Any) -> None:\n        \"\"\"Run the FastAPI server.\n\n        This method automatically detects if running in an async environment\n        (Jupyter, Colab, IPython, etc.) and:\n        - Uses appropriate async handling for existing event loops\n        - Sets up port forwarding if in Google Colab\n        - Displays the correct URL for accessing the app\n\n        Args:\n            **kwargs: Arguments passed to uvicorn configuration\n        \"\"\"\n        import sys\n        import asyncio\n        import uvicorn\n\n        # Check if we're in an environment with a running event loop FIRST\n        in_async_env = False\n        try:\n            asyncio.get_running_loop()\n            in_async_env = True\n        except RuntimeError:\n            in_async_env = False\n\n        # If in async environment, apply nest_asyncio BEFORE creating the app\n        if in_async_env:\n            try:\n                import nest_asyncio\n\n                nest_asyncio.apply()\n            except ImportError:\n                print(\"Warning: nest_asyncio not installed. Installing...\")\n                import subprocess\n\n                subprocess.check_call(\n                    [sys.executable, \"-m\", \"pip\", \"install\", \"nest_asyncio\"]\n                )\n                import nest_asyncio\n\n                nest_asyncio.apply()\n\n        # Now create the app after nest_asyncio is applied\n        app = self.create_app()\n\n        # Set defaults\n        run_kwargs = {\"host\": \"0.0.0.0\", \"port\": 8000, \"log_level\": \"info\", **kwargs}\n\n        # Get the port and other config from run_kwargs\n        port = run_kwargs.get(\"port\", 8000)\n        host = run_kwargs.get(\"host\", \"0.0.0.0\")\n        log_level = run_kwargs.get(\"log_level\", \"info\")\n\n        # Check if we're specifically in Google Colab for port forwarding\n        in_colab = \"google.colab\" in sys.modules\n\n        if in_colab:\n            try:\n                from google.colab import output\n\n                output.serve_kernel_port_as_window(port)\n                from google.colab.output import eval_js\n\n                print(\"Your app is running at:\")\n                print(eval_js(f\"google.colab.kernel.proxyPort({port})\"))\n            except Exception as e:\n                print(f\"Warning: Could not set up Colab port forwarding: {e}\")\n                print(f\"Your app is running at: http://localhost:{port}\")\n        else:\n            print(\"Your app is running at:\")\n            print(f\"http://localhost:{port}\")\n\n        if in_async_env:\n            # In Jupyter/Colab, create config with loop=\"asyncio\" and use asyncio.run()\n            # This matches the working pattern from Colab\n            config = uvicorn.Config(\n                app, host=host, port=port, log_level=log_level, loop=\"asyncio\"\n            )\n            server = uvicorn.Server(config)\n            asyncio.run(server.serve())\n        else:\n            # Normal execution outside of Jupyter/Colab\n            uvicorn.run(app, **run_kwargs)\n"
  },
  {
    "path": "src/vanna/servers/fastapi/routes.py",
    "content": "\"\"\"\nFastAPI route implementations for Vanna Agents.\n\"\"\"\n\nimport json\nimport traceback\nfrom typing import Any, AsyncGenerator, Dict, Optional\n\nfrom fastapi import FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect\nfrom fastapi.responses import StreamingResponse, HTMLResponse\n\nfrom ..base import ChatHandler, ChatRequest, ChatResponse\nfrom ..base.templates import get_index_html\nfrom ...core.user.request_context import RequestContext\n\n\ndef register_chat_routes(\n    app: FastAPI, chat_handler: ChatHandler, config: Optional[Dict[str, Any]] = None\n) -> None:\n    \"\"\"Register chat routes on FastAPI app.\n\n    Args:\n        app: FastAPI application\n        chat_handler: Chat handler instance\n        config: Server configuration\n    \"\"\"\n    config = config or {}\n\n    @app.get(\"/\", response_class=HTMLResponse)\n    async def index() -> str:\n        \"\"\"Serve the main chat interface.\"\"\"\n        dev_mode = config.get(\"dev_mode\", False)\n        cdn_url = config.get(\"cdn_url\", \"https://img.vanna.ai/vanna-components.js\")\n        api_base_url = config.get(\"api_base_url\", \"\")\n\n        return get_index_html(\n            dev_mode=dev_mode, cdn_url=cdn_url, api_base_url=api_base_url\n        )\n\n    @app.post(\"/api/vanna/v2/chat_sse\")\n    async def chat_sse(\n        chat_request: ChatRequest, http_request: Request\n    ) -> StreamingResponse:\n        \"\"\"Server-Sent Events endpoint for streaming chat.\"\"\"\n        # Extract request context for user resolution\n        chat_request.request_context = RequestContext(\n            cookies=dict(http_request.cookies),\n            headers=dict(http_request.headers),\n            remote_addr=http_request.client.host if http_request.client else None,\n            query_params=dict(http_request.query_params),\n            metadata=chat_request.metadata,\n        )\n\n        async def generate() -> AsyncGenerator[str, None]:\n            \"\"\"Generate SSE stream.\"\"\"\n            try:\n                async for chunk in chat_handler.handle_stream(chat_request):\n                    chunk_json = chunk.model_dump_json()\n                    yield f\"data: {chunk_json}\\n\\n\"\n                yield \"data: [DONE]\\n\\n\"\n            except Exception as e:\n                traceback.print_stack()\n                traceback.print_exc()\n                error_data = {\n                    \"type\": \"error\",\n                    \"data\": {\"message\": str(e)},\n                    \"conversation_id\": chat_request.conversation_id or \"\",\n                    \"request_id\": chat_request.request_id or \"\",\n                }\n                yield f\"data: {json.dumps(error_data)}\\n\\n\"\n\n        return StreamingResponse(\n            generate(),\n            media_type=\"text/event-stream\",\n            headers={\n                \"Cache-Control\": \"no-cache\",\n                \"Connection\": \"keep-alive\",\n                \"X-Accel-Buffering\": \"no\",  # Disable nginx buffering\n            },\n        )\n\n    @app.websocket(\"/api/vanna/v2/chat_websocket\")\n    async def chat_websocket(websocket: WebSocket) -> None:\n        \"\"\"WebSocket endpoint for real-time chat.\"\"\"\n        await websocket.accept()\n\n        try:\n            while True:\n                # Receive message\n                try:\n                    data = await websocket.receive_json()\n\n                    # Extract request context for user resolution\n                    metadata = data.get(\"metadata\", {})\n                    data[\"request_context\"] = RequestContext(\n                        cookies=dict(websocket.cookies),\n                        headers=dict(websocket.headers),\n                        remote_addr=websocket.client.host if websocket.client else None,\n                        query_params=dict(websocket.query_params),\n                        metadata=metadata,\n                    )\n\n                    chat_request = ChatRequest(**data)\n                except Exception as e:\n                    traceback.print_stack()\n                    traceback.print_exc()\n                    await websocket.send_json(\n                        {\n                            \"type\": \"error\",\n                            \"data\": {\"message\": f\"Invalid request: {str(e)}\"},\n                        }\n                    )\n                    continue\n\n                # Stream response\n                try:\n                    async for chunk in chat_handler.handle_stream(chat_request):\n                        await websocket.send_json(chunk.model_dump())\n\n                    # Send completion signal\n                    await websocket.send_json(\n                        {\n                            \"type\": \"completion\",\n                            \"data\": {\"status\": \"done\"},\n                            \"conversation_id\": chunk.conversation_id\n                            if \"chunk\" in locals()\n                            else \"\",\n                            \"request_id\": chunk.request_id\n                            if \"chunk\" in locals()\n                            else \"\",\n                        }\n                    )\n\n                except Exception as e:\n                    traceback.print_stack()\n                    traceback.print_exc()\n                    await websocket.send_json(\n                        {\n                            \"type\": \"error\",\n                            \"data\": {\"message\": str(e)},\n                            \"conversation_id\": chat_request.conversation_id or \"\",\n                            \"request_id\": chat_request.request_id or \"\",\n                        }\n                    )\n\n        except WebSocketDisconnect:\n            pass\n        except Exception as e:\n            traceback.print_stack()\n            traceback.print_exc()\n            try:\n                await websocket.send_json(\n                    {\n                        \"type\": \"error\",\n                        \"data\": {\"message\": f\"WebSocket error: {str(e)}\"},\n                    }\n                )\n            except Exception:\n                pass\n            finally:\n                await websocket.close()\n\n    @app.post(\"/api/vanna/v2/chat_poll\")\n    async def chat_poll(\n        chat_request: ChatRequest, http_request: Request\n    ) -> ChatResponse:\n        \"\"\"Polling endpoint for chat.\"\"\"\n        # Extract request context for user resolution\n        chat_request.request_context = RequestContext(\n            cookies=dict(http_request.cookies),\n            headers=dict(http_request.headers),\n            remote_addr=http_request.client.host if http_request.client else None,\n            query_params=dict(http_request.query_params),\n            metadata=chat_request.metadata,\n        )\n\n        try:\n            result = await chat_handler.handle_poll(chat_request)\n            return result\n        except Exception as e:\n            traceback.print_stack()\n            traceback.print_exc()\n            raise HTTPException(status_code=500, detail=f\"Chat failed: {str(e)}\")\n"
  },
  {
    "path": "src/vanna/servers/flask/__init__.py",
    "content": "\"\"\"\nFlask server implementation for Vanna Agents.\n\"\"\"\n\nfrom .app import VannaFlaskServer\n\n__all__ = [\"VannaFlaskServer\"]\n"
  },
  {
    "path": "src/vanna/servers/flask/app.py",
    "content": "\"\"\"\nFlask server factory for Vanna Agents.\n\"\"\"\n\nimport asyncio\nfrom typing import Any, Dict, Optional\n\nfrom flask import Flask\nfrom flask_cors import CORS\n\nfrom ...core import Agent\nfrom ..base import ChatHandler\nfrom .routes import register_chat_routes\n\n\nclass VannaFlaskServer:\n    \"\"\"Flask server factory for Vanna Agents.\"\"\"\n\n    def __init__(self, agent: Agent, config: Optional[Dict[str, Any]] = None):\n        \"\"\"Initialize Flask server.\n\n        Args:\n            agent: The agent to serve (must have user_resolver configured)\n            config: Optional server configuration\n        \"\"\"\n        self.agent = agent\n        self.config = config or {}\n        self.chat_handler = ChatHandler(agent)\n\n    def create_app(self) -> Flask:\n        \"\"\"Create configured Flask app.\n\n        Returns:\n            Configured Flask application\n        \"\"\"\n        # Check if dev mode is enabled\n        dev_mode = self.config.get(\"dev_mode\", False)\n        static_folder = self.config.get(\"static_folder\", \"static\") if dev_mode else None\n\n        app = Flask(__name__, static_folder=static_folder, static_url_path=\"/static\")\n\n        # Apply configuration\n        app.config.update(self.config.get(\"flask\", {}))\n\n        # Enable CORS if configured\n        cors_config = self.config.get(\"cors\", {})\n        if cors_config.get(\"enabled\", True):\n            CORS(app, **{k: v for k, v in cors_config.items() if k != \"enabled\"})\n\n        # Register routes\n        register_chat_routes(app, self.chat_handler, self.config)\n\n        # Add health check\n        @app.route(\"/health\")\n        def health_check() -> Dict[str, str]:\n            return {\"status\": \"healthy\", \"service\": \"vanna\"}\n\n        return app\n\n    def run(self, **kwargs: Any) -> None:\n        \"\"\"Run the Flask server.\n\n        This method automatically detects if running in an async environment\n        (Jupyter, Colab, IPython, etc.) and:\n        - Installs and applies nest_asyncio to handle existing event loops\n        - Sets up port forwarding if in Google Colab\n        - Displays the correct URL for accessing the app\n\n        Args:\n            **kwargs: Arguments passed to Flask.run()\n        \"\"\"\n        import sys\n\n        app = self.create_app()\n\n        # Set defaults\n        run_kwargs = {\"host\": \"0.0.0.0\", \"port\": 5000, \"debug\": False, **kwargs}\n\n        # Get the port from run_kwargs\n        port = run_kwargs.get(\"port\", 5000)\n\n        # Check if we're in an environment with a running event loop\n        # (Jupyter, Colab, IPython, VS Code notebooks, etc.)\n        in_async_env = False\n        try:\n            import asyncio\n\n            try:\n                asyncio.get_running_loop()\n                in_async_env = True\n            except RuntimeError:\n                in_async_env = False\n        except Exception:\n            pass\n\n        if in_async_env:\n            # Apply nest_asyncio to allow nested event loops\n            try:\n                import nest_asyncio\n\n                nest_asyncio.apply()\n            except ImportError:\n                print(\"Warning: nest_asyncio not installed. Installing...\")\n                import subprocess\n\n                subprocess.check_call(\n                    [sys.executable, \"-m\", \"pip\", \"install\", \"nest_asyncio\"]\n                )\n                import nest_asyncio\n\n                nest_asyncio.apply()\n\n        # Check if we're specifically in Google Colab for port forwarding\n        in_colab = \"google.colab\" in sys.modules\n\n        if in_colab:\n            try:\n                from google.colab import output\n\n                output.serve_kernel_port_as_window(port)\n                from google.colab.output import eval_js\n\n                print(\"Your app is running at:\")\n                print(eval_js(f\"google.colab.kernel.proxyPort({port})\"))\n            except Exception as e:\n                print(f\"Warning: Could not set up Colab port forwarding: {e}\")\n                print(f\"Your app is running at: http://localhost:{port}\")\n        else:\n            print(\"Your app is running at:\")\n            print(f\"http://localhost:{port}\")\n\n        app.run(**run_kwargs)\n"
  },
  {
    "path": "src/vanna/servers/flask/routes.py",
    "content": "\"\"\"\nFlask route implementations for Vanna Agents.\n\"\"\"\n\nimport asyncio\nimport json\nimport traceback\nfrom typing import Any, AsyncGenerator, Dict, Generator, Optional, Union\n\nfrom flask import Flask, Response, jsonify, request\n\nfrom ..base import ChatHandler, ChatRequest\nfrom ..base.templates import get_index_html\nfrom ...core.user.request_context import RequestContext\n\n\ndef register_chat_routes(\n    app: Flask, chat_handler: ChatHandler, config: Optional[Dict[str, Any]] = None\n) -> None:\n    \"\"\"Register chat routes on Flask app.\n\n    Args:\n        app: Flask application\n        chat_handler: Chat handler instance\n        config: Server configuration\n    \"\"\"\n    config = config or {}\n\n    @app.route(\"/\")\n    def index() -> str:\n        \"\"\"Serve the main chat interface.\"\"\"\n        dev_mode = config.get(\"dev_mode\", False)\n        cdn_url = config.get(\"cdn_url\", \"https://img.vanna.ai/vanna-components.js\")\n        api_base_url = config.get(\"api_base_url\", \"\")\n\n        return get_index_html(\n            dev_mode=dev_mode, cdn_url=cdn_url, api_base_url=api_base_url\n        )\n\n    @app.route(\"/api/vanna/v2/chat_sse\", methods=[\"POST\"])\n    def chat_sse() -> Union[Response, tuple[Response, int]]:\n        \"\"\"Server-Sent Events endpoint for streaming chat.\"\"\"\n        try:\n            data = request.get_json()\n            if not data:\n                return jsonify({\"error\": \"JSON body required\"}), 400\n\n            # Extract request context for user resolution\n            data[\"request_context\"] = RequestContext(\n                cookies=dict(request.cookies),\n                headers=dict(request.headers),\n                remote_addr=request.remote_addr,\n                query_params=dict(request.args),\n            )\n\n            chat_request = ChatRequest(**data)\n        except Exception as e:\n            traceback.print_stack()\n            traceback.print_exc()\n            return jsonify({\"error\": f\"Invalid request: {str(e)}\"}), 400\n\n        def generate() -> Generator[str, None, None]:\n            \"\"\"Generate SSE stream.\"\"\"\n            loop = asyncio.new_event_loop()\n            asyncio.set_event_loop(loop)\n\n            try:\n\n                async def async_generate() -> AsyncGenerator[str, None]:\n                    async for chunk in chat_handler.handle_stream(chat_request):\n                        chunk_json = chunk.model_dump_json()\n                        yield f\"data: {chunk_json}\\n\\n\"\n\n                gen = async_generate()\n                try:\n                    while True:\n                        chunk = loop.run_until_complete(gen.__anext__())\n                        yield chunk\n                except StopAsyncIteration:\n                    yield \"data: [DONE]\\n\\n\"\n            finally:\n                loop.close()\n\n        return Response(\n            generate(),\n            mimetype=\"text/event-stream\",\n            headers={\n                \"Cache-Control\": \"no-cache\",\n                \"Connection\": \"keep-alive\",\n                \"X-Accel-Buffering\": \"no\",  # Disable nginx buffering\n            },\n        )\n\n    @app.route(\"/api/vanna/v2/chat_websocket\")\n    def chat_websocket() -> tuple[Response, int]:\n        \"\"\"WebSocket endpoint placeholder.\"\"\"\n        return jsonify(\n            {\n                \"error\": \"WebSocket endpoint not implemented in basic Flask example\",\n                \"suggestion\": \"Use Flask-SocketIO for WebSocket support\",\n            }\n        ), 501\n\n    @app.route(\"/api/vanna/v2/chat_poll\", methods=[\"POST\"])\n    def chat_poll() -> Union[Response, tuple[Response, int]]:\n        \"\"\"Polling endpoint for chat.\"\"\"\n        try:\n            data = request.get_json()\n            if not data:\n                return jsonify({\"error\": \"JSON body required\"}), 400\n\n            # Extract request context for user resolution\n            data[\"request_context\"] = RequestContext(\n                cookies=dict(request.cookies),\n                headers=dict(request.headers),\n                remote_addr=request.remote_addr,\n                query_params=dict(request.args),\n            )\n\n            chat_request = ChatRequest(**data)\n        except Exception as e:\n            traceback.print_stack()\n            traceback.print_exc()\n            return jsonify({\"error\": f\"Invalid request: {str(e)}\"}), 400\n\n        # Run async handler in new event loop\n        loop = asyncio.new_event_loop()\n        asyncio.set_event_loop(loop)\n        try:\n            result = loop.run_until_complete(chat_handler.handle_poll(chat_request))\n            return jsonify(result.model_dump())\n        except Exception as e:\n            traceback.print_stack()\n            traceback.print_exc()\n            return jsonify({\"error\": f\"Chat failed: {str(e)}\"}), 500\n        finally:\n            loop.close()\n"
  },
  {
    "path": "src/vanna/tools/__init__.py",
    "content": "\"\"\"Built-in tool implementations.\"\"\"\n\nfrom .file_system import (\n    CommandResult,\n    FileSystem,\n    ListFilesTool,\n    LocalFileSystem,\n    ReadFileTool,\n    SearchFilesTool,\n    WriteFileTool,\n    create_file_system_tools,\n)\nfrom .python import (\n    PipInstallTool,\n    RunPythonFileTool,\n    create_python_tools,\n)\nfrom vanna.integrations.plotly import PlotlyChartGenerator\nfrom .run_sql import RunSqlTool\nfrom .visualize_data import VisualizeDataTool\n\n__all__ = [\n    # File system\n    \"FileSystem\",\n    \"LocalFileSystem\",\n    \"ListFilesTool\",\n    \"SearchFilesTool\",\n    \"ReadFileTool\",\n    \"WriteFileTool\",\n    \"create_file_system_tools\",\n    \"CommandResult\",\n    # Python tools\n    \"RunPythonFileTool\",\n    \"PipInstallTool\",\n    \"create_python_tools\",\n    # SQL\n    \"RunSqlTool\",\n    # Visualization\n    \"PlotlyChartGenerator\",\n    \"VisualizeDataTool\",\n]\n"
  },
  {
    "path": "src/vanna/tools/agent_memory.py",
    "content": "\"\"\"\nAgent memory tools.\n\nThis module provides agent memory operations through an abstract AgentMemory interface,\nallowing for different implementations (local vector DB, remote cloud service, etc.).\nThe tools access AgentMemory via ToolContext, which is populated by the Agent.\n\"\"\"\n\nimport logging\nfrom typing import Any, Dict, List, Optional, Type\nfrom pydantic import BaseModel, Field\n\nlogger = logging.getLogger(__name__)\n\nfrom vanna.core.tool import Tool, ToolContext, ToolResult\nfrom vanna.core.agent.config import UiFeature\nfrom vanna.capabilities.agent_memory import AgentMemory\nfrom vanna.components import (\n    UiComponent,\n    StatusBarUpdateComponent,\n    CardComponent,\n)\n\n\nclass SaveQuestionToolArgsParams(BaseModel):\n    \"\"\"Parameters for saving question-tool-argument combinations.\"\"\"\n\n    question: str = Field(description=\"The original question that was asked\")\n    tool_name: str = Field(\n        description=\"The name of the tool that was used successfully\"\n    )\n    args: Dict[str, Any] = Field(\n        description=\"The arguments that were passed to the tool\"\n    )\n\n\nclass SearchSavedCorrectToolUsesParams(BaseModel):\n    \"\"\"Parameters for searching saved tool usage patterns.\"\"\"\n\n    question: str = Field(\n        description=\"The question to find similar tool usage patterns for\"\n    )\n    limit: Optional[int] = Field(\n        default=10, description=\"Maximum number of results to return\"\n    )\n    similarity_threshold: Optional[float] = Field(\n        default=0.7, description=\"Minimum similarity score for results (0.0-1.0)\"\n    )\n    tool_name_filter: Optional[str] = Field(\n        default=None, description=\"Filter results to specific tool name\"\n    )\n\n\nclass SaveTextMemoryParams(BaseModel):\n    \"\"\"Parameters for saving free-form text memories.\"\"\"\n\n    content: str = Field(description=\"The text content to save as a memory\")\n\n\nclass SaveQuestionToolArgsTool(Tool[SaveQuestionToolArgsParams]):\n    \"\"\"Tool for saving successful question-tool-argument combinations.\"\"\"\n\n    @property\n    def name(self) -> str:\n        return \"save_question_tool_args\"\n\n    @property\n    def description(self) -> str:\n        return (\n            \"Save a successful question-tool-argument combination for future reference\"\n        )\n\n    def get_args_schema(self) -> Type[SaveQuestionToolArgsParams]:\n        return SaveQuestionToolArgsParams\n\n    async def execute(\n        self, context: ToolContext, args: SaveQuestionToolArgsParams\n    ) -> ToolResult:\n        \"\"\"Save the tool usage pattern to agent memory.\"\"\"\n        try:\n            await context.agent_memory.save_tool_usage(\n                question=args.question,\n                tool_name=args.tool_name,\n                args=args.args,\n                context=context,\n                success=True,\n            )\n\n            success_msg = (\n                f\"Successfully saved usage pattern for '{args.tool_name}' tool\"\n            )\n            return ToolResult(\n                success=True,\n                result_for_llm=success_msg,\n                ui_component=UiComponent(\n                    rich_component=StatusBarUpdateComponent(\n                        status=\"success\",\n                        message=\"Saved to memory\",\n                        detail=f\"Saved pattern for '{args.tool_name}'\",\n                    ),\n                    simple_component=None,\n                ),\n            )\n\n        except Exception as e:\n            error_message = f\"Failed to save memory: {str(e)}\"\n            return ToolResult(\n                success=False,\n                result_for_llm=error_message,\n                ui_component=UiComponent(\n                    rich_component=StatusBarUpdateComponent(\n                        status=\"error\", message=\"Failed to save memory\", detail=str(e)\n                    ),\n                    simple_component=None,\n                ),\n                error=str(e),\n            )\n\n\nclass SearchSavedCorrectToolUsesTool(Tool[SearchSavedCorrectToolUsesParams]):\n    \"\"\"Tool for searching saved tool usage patterns.\"\"\"\n\n    @property\n    def name(self) -> str:\n        return \"search_saved_correct_tool_uses\"\n\n    @property\n    def description(self) -> str:\n        return \"Search for similar tool usage patterns based on a question\"\n\n    def get_args_schema(self) -> Type[SearchSavedCorrectToolUsesParams]:\n        return SearchSavedCorrectToolUsesParams\n\n    async def execute(\n        self, context: ToolContext, args: SearchSavedCorrectToolUsesParams\n    ) -> ToolResult:\n        \"\"\"Search for similar tool usage patterns.\"\"\"\n        try:\n            results = await context.agent_memory.search_similar_usage(\n                question=args.question,\n                context=context,\n                limit=args.limit or 10,\n                similarity_threshold=args.similarity_threshold or 0.7,\n                tool_name_filter=args.tool_name_filter,\n            )\n\n            if not results:\n                no_results_msg = (\n                    \"No similar tool usage patterns found for this question.\"\n                )\n\n                # Check if user has access to detailed memory results\n                ui_features_available = context.metadata.get(\n                    \"ui_features_available\", []\n                )\n                show_detailed_results = (\n                    UiFeature.UI_FEATURE_SHOW_MEMORY_DETAILED_RESULTS\n                    in ui_features_available\n                )\n\n                # Create UI component based on access level\n                if show_detailed_results:\n                    # Admin view: Show card indicating 0 results\n                    ui_component = UiComponent(\n                        rich_component=CardComponent(\n                            title=\"🧠 Memory Search: 0 Results\",\n                            content=\"No similar tool usage patterns found for this question.\\n\\nSearched agent memory with no matches.\",\n                            icon=\"🔍\",\n                            status=\"info\",\n                            collapsible=True,\n                            collapsed=True,\n                            markdown=True,\n                        ),\n                        simple_component=None,\n                    )\n                else:\n                    # Non-admin view: Simple status message\n                    ui_component = UiComponent(\n                        rich_component=StatusBarUpdateComponent(\n                            status=\"idle\",\n                            message=\"No similar patterns found\",\n                            detail=\"Searched agent memory\",\n                        ),\n                        simple_component=None,\n                    )\n\n                return ToolResult(\n                    success=True,\n                    result_for_llm=no_results_msg,\n                    ui_component=ui_component,\n                )\n\n            # Format results for LLM\n            results_text = f\"Found {len(results)} similar tool usage pattern(s):\\n\\n\"\n            for i, result in enumerate(results, 1):\n                memory = result.memory\n                results_text += f\"{i}. {memory.tool_name} (similarity: {result.similarity_score:.2f})\\n\"\n                results_text += f\"   Question: {memory.question}\\n\"\n                results_text += f\"   Args: {memory.args}\\n\\n\"\n\n            logger.info(f\"Agent memory search results: {results_text.strip()}\")\n\n            # Check if user has access to detailed memory results\n            ui_features_available = context.metadata.get(\"ui_features_available\", [])\n            show_detailed_results = (\n                UiFeature.UI_FEATURE_SHOW_MEMORY_DETAILED_RESULTS\n                in ui_features_available\n            )\n\n            # Create UI component based on access level\n            if show_detailed_results:\n                # Admin view: Show detailed results in collapsible card\n                detailed_content = \"**Retrieved memories passed to LLM:**\\n\\n\"\n                for i, result in enumerate(results, 1):\n                    memory = result.memory\n                    detailed_content += f\"**{i}. {memory.tool_name}** (similarity: {result.similarity_score:.2f})\\n\"\n                    detailed_content += f\"- **Question:** {memory.question}\\n\"\n                    detailed_content += f\"- **Arguments:** `{memory.args}`\\n\"\n                    if memory.timestamp:\n                        detailed_content += f\"- **Timestamp:** {memory.timestamp}\\n\"\n                    if memory.memory_id:\n                        detailed_content += f\"- **ID:** `{memory.memory_id}`\\n\"\n                    detailed_content += \"\\n\"\n\n                ui_component = UiComponent(\n                    rich_component=CardComponent(\n                        title=f\"🧠 Memory Search: {len(results)} Result(s)\",\n                        content=detailed_content.strip(),\n                        icon=\"🔍\",\n                        status=\"info\",\n                        collapsible=True,\n                        collapsed=True,  # Start collapsed to avoid clutter\n                        markdown=True,  # Render content as markdown\n                    ),\n                    simple_component=None,\n                )\n            else:\n                # Non-admin view: Simple status message\n                ui_component = UiComponent(\n                    rich_component=StatusBarUpdateComponent(\n                        status=\"success\",\n                        message=f\"Found {len(results)} similar pattern(s)\",\n                        detail=\"Retrieved from agent memory\",\n                    ),\n                    simple_component=None,\n                )\n\n            return ToolResult(\n                success=True,\n                result_for_llm=results_text.strip(),\n                ui_component=ui_component,\n            )\n\n        except Exception as e:\n            error_message = f\"Failed to search memories: {str(e)}\"\n            return ToolResult(\n                success=False,\n                result_for_llm=error_message,\n                ui_component=UiComponent(\n                    rich_component=StatusBarUpdateComponent(\n                        status=\"error\", message=\"Failed to search memory\", detail=str(e)\n                    ),\n                    simple_component=None,\n                ),\n                error=str(e),\n            )\n\n\nclass SaveTextMemoryTool(Tool[SaveTextMemoryParams]):\n    \"\"\"Tool for saving free-form text memories.\"\"\"\n\n    @property\n    def name(self) -> str:\n        return \"save_text_memory\"\n\n    @property\n    def description(self) -> str:\n        return \"Save free-form text memory for important insights, observations, or context\"\n\n    def get_args_schema(self) -> Type[SaveTextMemoryParams]:\n        return SaveTextMemoryParams\n\n    async def execute(\n        self, context: ToolContext, args: SaveTextMemoryParams\n    ) -> ToolResult:\n        \"\"\"Save a text memory to agent memory.\"\"\"\n        try:\n            text_memory = await context.agent_memory.save_text_memory(\n                content=args.content, context=context\n            )\n\n            success_msg = (\n                f\"Successfully saved text memory with ID: {text_memory.memory_id}\"\n            )\n            return ToolResult(\n                success=True,\n                result_for_llm=success_msg,\n                ui_component=UiComponent(\n                    rich_component=StatusBarUpdateComponent(\n                        status=\"success\",\n                        message=\"Saved text memory\",\n                        detail=f\"ID: {text_memory.memory_id}\",\n                    ),\n                    simple_component=None,\n                ),\n            )\n\n        except Exception as e:\n            error_message = f\"Failed to save text memory: {str(e)}\"\n            return ToolResult(\n                success=False,\n                result_for_llm=error_message,\n                ui_component=UiComponent(\n                    rich_component=StatusBarUpdateComponent(\n                        status=\"error\",\n                        message=\"Failed to save text memory\",\n                        detail=str(e),\n                    ),\n                    simple_component=None,\n                ),\n                error=str(e),\n            )\n"
  },
  {
    "path": "src/vanna/tools/file_system.py",
    "content": "\"\"\"\nFile system tools with dependency injection support.\n\nThis module provides file system operations through an abstract FileSystem interface,\nallowing for different implementations (local, remote, sandboxed, etc.).\nThe tools accept a FileSystem instance via dependency injection.\n\"\"\"\n\nimport asyncio\nfrom abc import ABC, abstractmethod\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import Any, List, Optional, Type\nimport difflib\nimport hashlib\n\nfrom pydantic import BaseModel, Field, model_validator\n\nfrom vanna.core.tool import Tool, ToolContext, ToolResult\nfrom vanna.components import (\n    UiComponent,\n    CardComponent,\n    NotificationComponent,\n    ComponentType,\n    SimpleTextComponent,\n)\n\nMAX_SEARCH_FILE_BYTES = 1_000_000\nFILENAME_MATCH_SNIPPET = \"[filename match]\"\n\n\n@dataclass\nclass FileSearchMatch:\n    \"\"\"Represents a single search result within a file system.\"\"\"\n\n    path: str\n    snippet: Optional[str] = None\n\n\n@dataclass\nclass CommandResult:\n    \"\"\"Represents the result of executing a shell command.\"\"\"\n\n    stdout: str\n    stderr: str\n    returncode: int\n\n\ndef _make_snippet(text: str, query: str, context_window: int = 60) -> Optional[str]:\n    \"\"\"Return a short snippet around the first occurrence of query in text.\"\"\"\n\n    lowered = text.lower()\n    index = lowered.find(query.lower())\n    if index == -1:\n        return None\n\n    start = max(0, index - context_window)\n    end = min(len(text), index + len(query) + context_window)\n    snippet = text[start:end].replace(\"\\n\", \" \").strip()\n\n    if start > 0:\n        snippet = f\"…{snippet}\"\n    if end < len(text):\n        snippet = f\"{snippet}…\"\n\n    return snippet\n\n\nclass FileSystem(ABC):\n    \"\"\"Abstract base class for file system operations.\"\"\"\n\n    @abstractmethod\n    async def list_files(self, directory: str, context: ToolContext) -> List[str]:\n        \"\"\"List files in a directory.\"\"\"\n        pass\n\n    @abstractmethod\n    async def read_file(self, filename: str, context: ToolContext) -> str:\n        \"\"\"Read the contents of a file.\"\"\"\n        pass\n\n    @abstractmethod\n    async def write_file(\n        self, filename: str, content: str, context: ToolContext, overwrite: bool = False\n    ) -> None:\n        \"\"\"Write content to a file.\"\"\"\n        pass\n\n    @abstractmethod\n    async def exists(self, path: str, context: ToolContext) -> bool:\n        \"\"\"Check if a file or directory exists.\"\"\"\n        pass\n\n    @abstractmethod\n    async def is_directory(self, path: str, context: ToolContext) -> bool:\n        \"\"\"Check if a path is a directory.\"\"\"\n        pass\n\n    @abstractmethod\n    async def search_files(\n        self,\n        query: str,\n        context: ToolContext,\n        *,\n        max_results: int = 20,\n        include_content: bool = False,\n    ) -> List[FileSearchMatch]:\n        \"\"\"Search for files matching a query within the accessible namespace.\"\"\"\n        pass\n\n    @abstractmethod\n    async def run_bash(\n        self,\n        command: str,\n        context: ToolContext,\n        *,\n        timeout: Optional[float] = None,\n    ) -> CommandResult:\n        \"\"\"Execute a bash command within the accessible namespace.\"\"\"\n        pass\n\n\nclass LocalFileSystem(FileSystem):\n    \"\"\"Local file system implementation with per-user isolation.\"\"\"\n\n    def __init__(self, working_directory: str = \".\"):\n        \"\"\"Initialize with a working directory.\n\n        Args:\n            working_directory: Base directory where user-specific folders will be created\n        \"\"\"\n        self.working_directory = Path(working_directory)\n\n    def _get_user_directory(self, context: ToolContext) -> Path:\n        \"\"\"Get the user-specific directory by hashing the user ID.\n\n        Args:\n            context: Tool context containing user information\n\n        Returns:\n            Path to the user-specific directory\n        \"\"\"\n        # Hash the user ID to create a directory name\n        user_hash = hashlib.sha256(context.user.id.encode()).hexdigest()[:16]\n        user_dir = self.working_directory / user_hash\n\n        # Create the directory if it doesn't exist\n        user_dir.mkdir(parents=True, exist_ok=True)\n\n        return user_dir\n\n    def _resolve_path(self, path: str, context: ToolContext) -> Path:\n        \"\"\"Resolve a path relative to the user's directory.\n\n        Args:\n            path: Path relative to user directory\n            context: Tool context containing user information\n\n        Returns:\n            Absolute path within user's directory\n        \"\"\"\n        user_dir = self._get_user_directory(context)\n        resolved = user_dir / path\n\n        # Ensure the path is within the user's directory (prevent directory traversal)\n        try:\n            resolved.resolve().relative_to(user_dir.resolve())\n        except ValueError:\n            raise PermissionError(\n                f\"Access denied: path '{path}' is outside user directory\"\n            )\n\n        return resolved\n\n    async def list_files(self, directory: str, context: ToolContext) -> List[str]:\n        \"\"\"List files in a directory within the user's isolated space.\"\"\"\n        directory_path = self._resolve_path(directory, context)\n\n        if not directory_path.exists():\n            raise FileNotFoundError(f\"Directory '{directory}' does not exist\")\n\n        if not directory_path.is_dir():\n            raise NotADirectoryError(f\"'{directory}' is not a directory\")\n\n        files = []\n        for item in directory_path.iterdir():\n            if item.is_file():\n                files.append(item.name)\n\n        return sorted(files)\n\n    async def read_file(self, filename: str, context: ToolContext) -> str:\n        \"\"\"Read the contents of a file within the user's isolated space.\"\"\"\n        file_path = self._resolve_path(filename, context)\n\n        if not file_path.exists():\n            raise FileNotFoundError(f\"File '{filename}' does not exist\")\n\n        if not file_path.is_file():\n            raise IsADirectoryError(f\"'{filename}' is a directory, not a file\")\n\n        return file_path.read_text(encoding=\"utf-8\")\n\n    async def write_file(\n        self, filename: str, content: str, context: ToolContext, overwrite: bool = False\n    ) -> None:\n        \"\"\"Write content to a file within the user's isolated space.\"\"\"\n        file_path = self._resolve_path(filename, context)\n\n        # Create parent directories if they don't exist\n        file_path.parent.mkdir(parents=True, exist_ok=True)\n\n        if file_path.exists() and not overwrite:\n            raise FileExistsError(\n                f\"File '{filename}' already exists. Use overwrite=True to replace it.\"\n            )\n\n        file_path.write_text(content, encoding=\"utf-8\")\n\n    async def exists(self, path: str, context: ToolContext) -> bool:\n        \"\"\"Check if a file or directory exists within the user's isolated space.\"\"\"\n        try:\n            resolved_path = self._resolve_path(path, context)\n            return resolved_path.exists()\n        except PermissionError:\n            return False\n\n    async def is_directory(self, path: str, context: ToolContext) -> bool:\n        \"\"\"Check if a path is a directory within the user's isolated space.\"\"\"\n        try:\n            resolved_path = self._resolve_path(path, context)\n            return resolved_path.exists() and resolved_path.is_dir()\n        except PermissionError:\n            return False\n\n    async def search_files(\n        self,\n        query: str,\n        context: ToolContext,\n        *,\n        max_results: int = 20,\n        include_content: bool = False,\n    ) -> List[FileSearchMatch]:\n        \"\"\"Search for files within the user's isolated space.\"\"\"\n\n        trimmed_query = query.strip()\n        if not trimmed_query:\n            raise ValueError(\"Search query must not be empty\")\n\n        user_dir = self._get_user_directory(context)\n        matches: List[FileSearchMatch] = []\n        query_lower = trimmed_query.lower()\n\n        for path in user_dir.rglob(\"*\"):\n            if len(matches) >= max_results:\n                break\n\n            if not path.is_file():\n                continue\n\n            relative_path = path.relative_to(user_dir).as_posix()\n            include_entry = False\n            snippet: Optional[str] = None\n\n            if query_lower in path.name.lower():\n                include_entry = True\n                snippet = FILENAME_MATCH_SNIPPET\n\n            content: Optional[str] = None\n            if include_content:\n                try:\n                    size = path.stat().st_size\n                except OSError:\n                    if include_entry:\n                        matches.append(\n                            FileSearchMatch(path=relative_path, snippet=snippet)\n                        )\n                    continue\n\n                if size <= MAX_SEARCH_FILE_BYTES:\n                    try:\n                        content = path.read_text(encoding=\"utf-8\")\n                    except (UnicodeDecodeError, OSError):\n                        content = None\n                elif not include_entry:\n                    # Skip oversized files if they do not match by name\n                    continue\n\n            if include_content and content is not None:\n                if query_lower in content.lower():\n                    snippet = _make_snippet(content, trimmed_query) or snippet\n                    include_entry = True\n                elif not include_entry:\n                    continue\n\n            if include_entry:\n                matches.append(FileSearchMatch(path=relative_path, snippet=snippet))\n\n        return matches\n\n    async def run_bash(\n        self,\n        command: str,\n        context: ToolContext,\n        *,\n        timeout: Optional[float] = None,\n    ) -> CommandResult:\n        \"\"\"Execute a bash command within the user's isolated space.\"\"\"\n\n        if not command.strip():\n            raise ValueError(\"Command must not be empty\")\n\n        user_dir = self._get_user_directory(context)\n\n        process = await asyncio.create_subprocess_shell(\n            command,\n            stdout=asyncio.subprocess.PIPE,\n            stderr=asyncio.subprocess.PIPE,\n            cwd=str(user_dir),\n        )\n\n        try:\n            stdout_bytes, stderr_bytes = await asyncio.wait_for(\n                process.communicate(), timeout=timeout\n            )\n        except asyncio.TimeoutError as exc:\n            process.kill()\n            await process.wait()\n            raise TimeoutError(f\"Command timed out after {timeout} seconds\") from exc\n\n        stdout = stdout_bytes.decode(\"utf-8\", errors=\"replace\")\n        stderr = stderr_bytes.decode(\"utf-8\", errors=\"replace\")\n\n        return CommandResult(\n            stdout=stdout, stderr=stderr, returncode=process.returncode or 0\n        )\n\n\nclass SearchFilesArgs(BaseModel):\n    \"\"\"Arguments for searching files.\"\"\"\n\n    query: str = Field(description=\"Text to search for in file names or contents\")\n    include_content: bool = Field(\n        default=True,\n        description=\"Whether to search within file contents in addition to file names\",\n    )\n    max_results: int = Field(\n        default=20,\n        ge=1,\n        le=100,\n        description=\"Maximum number of matches to return\",\n    )\n\n\nclass SearchFilesTool(Tool[SearchFilesArgs]):\n    \"\"\"Tool to search for files using the injected file system implementation.\"\"\"\n\n    def __init__(self, file_system: Optional[FileSystem] = None):\n        self.file_system = file_system or LocalFileSystem()\n\n    @property\n    def name(self) -> str:\n        return \"search_files\"\n\n    @property\n    def description(self) -> str:\n        return \"Search for files by name or content\"\n\n    def get_args_schema(self) -> Type[SearchFilesArgs]:\n        return SearchFilesArgs\n\n    async def execute(self, context: ToolContext, args: SearchFilesArgs) -> ToolResult:\n        try:\n            matches = await self.file_system.search_files(\n                args.query,\n                context,\n                max_results=args.max_results,\n                include_content=args.include_content,\n            )\n        except Exception as exc:\n            error_msg = f\"Error searching files: {exc}\"\n            return ToolResult(\n                success=False,\n                result_for_llm=error_msg,\n                ui_component=UiComponent(\n                    rich_component=NotificationComponent(\n                        type=ComponentType.NOTIFICATION,\n                        level=\"error\",\n                        message=error_msg,\n                    ),\n                    simple_component=SimpleTextComponent(text=error_msg),\n                ),\n                error=str(exc),\n            )\n\n        if not matches:\n            message = f\"No matches found for '{args.query}'.\"\n            return ToolResult(\n                success=True,\n                result_for_llm=message,\n                ui_component=UiComponent(\n                    rich_component=NotificationComponent(\n                        type=ComponentType.NOTIFICATION,\n                        level=\"info\",\n                        message=message,\n                    ),\n                    simple_component=SimpleTextComponent(text=message),\n                ),\n            )\n\n        lines: List[str] = []\n        for match in matches:\n            snippet = match.snippet\n            if snippet == FILENAME_MATCH_SNIPPET:\n                snippet_text = \"(matched filename)\"\n            elif snippet:\n                snippet_text = snippet\n            else:\n                snippet_text = \"\"\n\n            if snippet_text and len(snippet_text) > 200:\n                snippet_text = f\"{snippet_text[:197]}…\"\n\n            if snippet_text:\n                lines.append(f\"- {match.path}: {snippet_text}\")\n            else:\n                lines.append(f\"- {match.path}\")\n\n        summary = f\"Found {len(matches)} match(es) for '{args.query}' (max {args.max_results}).\"\n        content = \"\\n\".join(lines)\n\n        return ToolResult(\n            success=True,\n            result_for_llm=f\"{summary}\\n{content}\",\n            ui_component=UiComponent(\n                rich_component=CardComponent(\n                    type=ComponentType.CARD,\n                    title=f\"Search results for '{args.query}'\",\n                    content=content,\n                ),\n                simple_component=SimpleTextComponent(text=summary),\n            ),\n        )\n\n\nclass ListFilesArgs(BaseModel):\n    \"\"\"Arguments for listing files.\"\"\"\n\n    directory: str = Field(\n        default=\".\", description=\"Directory to list (defaults to current)\"\n    )\n\n\nclass ListFilesTool(Tool[ListFilesArgs]):\n    \"\"\"Tool to list files in a directory using dependency injection for file system access.\"\"\"\n\n    def __init__(self, file_system: Optional[FileSystem] = None):\n        \"\"\"Initialize with optional file system dependency.\"\"\"\n        self.file_system = file_system or LocalFileSystem()\n\n    @property\n    def name(self) -> str:\n        return \"list_files\"\n\n    @property\n    def description(self) -> str:\n        return \"List files in a directory\"\n\n    def get_args_schema(self) -> Type[ListFilesArgs]:\n        return ListFilesArgs\n\n    async def execute(self, context: ToolContext, args: ListFilesArgs) -> ToolResult:\n        try:\n            files = await self.file_system.list_files(args.directory, context)\n\n            if not files:\n                result = f\"No files found in directory '{args.directory}'\"\n                files_list = \"No files found\"\n            else:\n                files_list = \"\\n\".join(f\"- {f}\" for f in files)\n                result = f\"Files in '{args.directory}':\\n{files_list}\"\n\n            return ToolResult(\n                success=True,\n                result_for_llm=result,\n                ui_component=UiComponent(\n                    rich_component=CardComponent(\n                        type=ComponentType.CARD,\n                        title=f\"Files in {args.directory}\",\n                        content=files_list,\n                    ),\n                    simple_component=SimpleTextComponent(text=result),\n                ),\n            )\n        except Exception as e:\n            error_msg = f\"Error listing files: {str(e)}\"\n            return ToolResult(\n                success=False,\n                result_for_llm=error_msg,\n                ui_component=UiComponent(\n                    rich_component=NotificationComponent(\n                        type=ComponentType.NOTIFICATION,\n                        level=\"error\",\n                        message=error_msg,\n                    ),\n                    simple_component=SimpleTextComponent(text=error_msg),\n                ),\n                error=str(e),\n            )\n\n\nclass ReadFileArgs(BaseModel):\n    \"\"\"Arguments for reading a file.\"\"\"\n\n    filename: str = Field(description=\"Name of the file to read\")\n\n\nclass ReadFileTool(Tool[ReadFileArgs]):\n    \"\"\"Tool to read file contents using dependency injection for file system access.\"\"\"\n\n    def __init__(self, file_system: Optional[FileSystem] = None):\n        \"\"\"Initialize with optional file system dependency.\"\"\"\n        self.file_system = file_system or LocalFileSystem()\n\n    @property\n    def name(self) -> str:\n        return \"read_file\"\n\n    @property\n    def description(self) -> str:\n        return \"Read the contents of a file\"\n\n    def get_args_schema(self) -> Type[ReadFileArgs]:\n        return ReadFileArgs\n\n    async def execute(self, context: ToolContext, args: ReadFileArgs) -> ToolResult:\n        try:\n            content = await self.file_system.read_file(args.filename, context)\n            result = f\"Content of '{args.filename}':\\n\\n{content}\"\n\n            return ToolResult(\n                success=True,\n                result_for_llm=result,\n                ui_component=UiComponent(\n                    rich_component=CardComponent(\n                        type=ComponentType.CARD,\n                        title=f\"Contents of {args.filename}\",\n                        content=content,\n                    ),\n                    simple_component=SimpleTextComponent(\n                        text=f\"File content:\\n{content}\"\n                    ),\n                ),\n            )\n        except Exception as e:\n            error_msg = f\"Error reading file: {str(e)}\"\n            return ToolResult(\n                success=False,\n                result_for_llm=error_msg,\n                ui_component=UiComponent(\n                    rich_component=NotificationComponent(\n                        type=ComponentType.NOTIFICATION,\n                        level=\"error\",\n                        message=error_msg,\n                    ),\n                    simple_component=SimpleTextComponent(text=error_msg),\n                ),\n                error=str(e),\n            )\n\n\nclass WriteFileArgs(BaseModel):\n    \"\"\"Arguments for writing a file.\"\"\"\n\n    filename: str = Field(description=\"Name of the file to write\")\n    content: str = Field(description=\"Content to write to the file\")\n    overwrite: bool = Field(\n        default=False, description=\"Whether to overwrite existing files\"\n    )\n\n\nclass WriteFileTool(Tool[WriteFileArgs]):\n    \"\"\"Tool to write content to a file using dependency injection for file system access.\"\"\"\n\n    def __init__(self, file_system: Optional[FileSystem] = None):\n        \"\"\"Initialize with optional file system dependency.\"\"\"\n        self.file_system = file_system or LocalFileSystem()\n\n    @property\n    def name(self) -> str:\n        return \"write_file\"\n\n    @property\n    def description(self) -> str:\n        return \"Write content to a file\"\n\n    def get_args_schema(self) -> Type[WriteFileArgs]:\n        return WriteFileArgs\n\n    async def execute(self, context: ToolContext, args: WriteFileArgs) -> ToolResult:\n        try:\n            await self.file_system.write_file(\n                args.filename, args.content, context, args.overwrite\n            )\n\n            success_msg = f\"Successfully wrote {len(args.content)} characters to '{args.filename}'\"\n\n            return ToolResult(\n                success=True,\n                result_for_llm=success_msg,\n                ui_component=UiComponent(\n                    rich_component=NotificationComponent(\n                        type=ComponentType.NOTIFICATION,\n                        level=\"success\",\n                        message=f\"File '{args.filename}' written successfully\",\n                    ),\n                    simple_component=SimpleTextComponent(\n                        text=f\"Wrote to {args.filename}\"\n                    ),\n                ),\n            )\n        except Exception as e:\n            error_msg = f\"Error writing file: {str(e)}\"\n            return ToolResult(\n                success=False,\n                result_for_llm=error_msg,\n                ui_component=UiComponent(\n                    rich_component=NotificationComponent(\n                        type=ComponentType.NOTIFICATION,\n                        level=\"error\",\n                        message=error_msg,\n                    ),\n                    simple_component=SimpleTextComponent(text=error_msg),\n                ),\n                error=str(e),\n            )\n\n\nclass LineEdit(BaseModel):\n    \"\"\"Definition of a single line-based edit operation.\"\"\"\n\n    start_line: int = Field(\n        ge=1, description=\"First line (1-based) affected by this edit\"\n    )\n    end_line: Optional[int] = Field(\n        default=None,\n        description=(\n            \"Last line (1-based, inclusive) to replace. Set to start_line - 1 to insert before start_line. \"\n            \"Defaults to start_line, replacing a single line.\"\n        ),\n    )\n    new_content: str = Field(\n        default=\"\", description=\"Replacement text (preserves provided newlines)\"\n    )\n\n    @model_validator(mode=\"after\")\n    def validate_line_range(self) -> \"LineEdit\":\n        effective_end = self.start_line if self.end_line is None else self.end_line\n\n        if effective_end < self.start_line - 1:\n            raise ValueError(\"end_line must be >= start_line - 1\")\n\n        return self\n\n\nclass EditFileArgs(BaseModel):\n    \"\"\"Arguments for editing one or more sections within a file.\"\"\"\n\n    filename: str = Field(description=\"Path to the file to edit\")\n    edits: List[LineEdit] = Field(\n        description=\"List of edits to apply. Later entries should reference higher line numbers.\",\n        min_length=1,\n    )\n\n\nclass EditFileTool(Tool[EditFileArgs]):\n    \"\"\"Tool to apply line-based edits to an existing file.\"\"\"\n\n    def __init__(self, file_system: Optional[FileSystem] = None):\n        self.file_system = file_system or LocalFileSystem()\n\n    @property\n    def name(self) -> str:\n        return \"edit_file\"\n\n    @property\n    def description(self) -> str:\n        return \"Modify specific lines within a file\"\n\n    def get_args_schema(self) -> Type[EditFileArgs]:\n        return EditFileArgs\n\n    async def execute(self, context: ToolContext, args: EditFileArgs) -> ToolResult:\n        try:\n            original_content = await self.file_system.read_file(args.filename, context)\n        except Exception as exc:\n            error_msg = f\"Error loading file '{args.filename}': {exc}\"\n            return ToolResult(\n                success=False,\n                result_for_llm=error_msg,\n                ui_component=UiComponent(\n                    rich_component=NotificationComponent(\n                        type=ComponentType.NOTIFICATION,\n                        level=\"error\",\n                        message=error_msg,\n                    ),\n                    simple_component=SimpleTextComponent(text=error_msg),\n                ),\n                error=str(exc),\n            )\n\n        lines = original_content.splitlines(keepends=True)\n        applied_edits: List[str] = []\n\n        # Apply edits starting from the bottom so line numbers remain valid for each operation\n        for edit in sorted(args.edits, key=lambda e: e.start_line, reverse=True):\n            start_line = edit.start_line\n            end_line = edit.end_line if edit.end_line is not None else edit.start_line\n\n            if start_line < 1:\n                return self._range_error(\n                    args.filename, start_line, end_line, \"start_line must be >= 1\"\n                )\n\n            if end_line < start_line - 1:\n                return self._range_error(\n                    args.filename,\n                    start_line,\n                    end_line,\n                    \"end_line must be >= start_line - 1\",\n                )\n\n            is_insertion = end_line == start_line - 1\n\n            if not is_insertion and start_line > len(lines):\n                return self._range_error(\n                    args.filename,\n                    start_line,\n                    end_line,\n                    f\"start_line {start_line} is beyond the end of the file (len={len(lines)})\",\n                )\n\n            if is_insertion:\n                if start_line > len(lines) + 1:\n                    return self._range_error(\n                        args.filename,\n                        start_line,\n                        end_line,\n                        \"Cannot insert beyond one line past the end of the file\",\n                    )\n                start_index = min(start_line - 1, len(lines))\n                end_index = start_index\n            else:\n                if end_line > len(lines):\n                    return self._range_error(\n                        args.filename,\n                        start_line,\n                        end_line,\n                        f\"end_line {end_line} is beyond the end of the file (len={len(lines)})\",\n                    )\n                start_index = start_line - 1\n                end_index = end_line\n\n            replacement_lines = edit.new_content.splitlines(keepends=True)\n            lines[start_index:end_index] = replacement_lines\n\n            if is_insertion:\n                inserted_count = len(replacement_lines)\n                applied_edits.append(\n                    f\"Inserted {inserted_count} line(s) at line {start_line}\"\n                )\n            else:\n                removed_count = end_line - start_line + 1\n                applied_edits.append(\n                    f\"Replaced lines {start_line}-{end_line} (removed {removed_count} line(s))\"\n                )\n\n        new_content = \"\".join(lines)\n\n        if new_content == original_content:\n            message = (\n                f\"No changes applied to '{args.filename}' (content already up to date).\"\n            )\n            return ToolResult(\n                success=True,\n                result_for_llm=message,\n                ui_component=UiComponent(\n                    rich_component=NotificationComponent(\n                        type=ComponentType.NOTIFICATION,\n                        level=\"info\",\n                        message=message,\n                    ),\n                    simple_component=SimpleTextComponent(text=message),\n                ),\n            )\n\n        try:\n            await self.file_system.write_file(\n                args.filename, new_content, context, overwrite=True\n            )\n        except Exception as exc:\n            error_msg = f\"Error writing updated contents to '{args.filename}': {exc}\"\n            return ToolResult(\n                success=False,\n                result_for_llm=error_msg,\n                ui_component=UiComponent(\n                    rich_component=NotificationComponent(\n                        type=ComponentType.NOTIFICATION,\n                        level=\"error\",\n                        message=error_msg,\n                    ),\n                    simple_component=SimpleTextComponent(text=error_msg),\n                ),\n                error=str(exc),\n            )\n\n        diff_lines = list(\n            difflib.unified_diff(\n                original_content.splitlines(),\n                new_content.splitlines(),\n                fromfile=f\"a/{args.filename}\",\n                tofile=f\"b/{args.filename}\",\n                lineterm=\"\",\n            )\n        )\n\n        diff_text = (\n            \"\\n\".join(diff_lines) if diff_lines else \"(No textual diff available)\"\n        )\n        summary = (\n            f\"Updated '{args.filename}' with {len(args.edits)} edit(s).\\n\"\n            + \"\\n\".join(reversed(applied_edits))\n        )\n\n        return ToolResult(\n            success=True,\n            result_for_llm=f\"{summary}\\n\\n{diff_text}\",\n            ui_component=UiComponent(\n                rich_component=CardComponent(\n                    type=ComponentType.CARD,\n                    title=f\"Edited {args.filename}\",\n                    content=diff_text,\n                ),\n                simple_component=SimpleTextComponent(text=summary),\n            ),\n        )\n\n    def _range_error(\n        self, filename: str, start_line: int, end_line: int, message: str\n    ) -> ToolResult:\n        error_msg = f\"Invalid edit range for '{filename}': start_line={start_line}, end_line={end_line}. {message}\"\n        return ToolResult(\n            success=False,\n            result_for_llm=error_msg,\n            ui_component=UiComponent(\n                rich_component=NotificationComponent(\n                    type=ComponentType.NOTIFICATION,\n                    level=\"error\",\n                    message=error_msg,\n                ),\n                simple_component=SimpleTextComponent(text=error_msg),\n            ),\n            error=message,\n        )\n\n\n# Convenience function for creating tools with default local file system\ndef create_file_system_tools(\n    file_system: Optional[FileSystem] = None,\n) -> List[Tool[Any]]:\n    \"\"\"Create a set of file system tools with optional dependency injection.\"\"\"\n    fs = file_system or LocalFileSystem()\n    return [\n        ListFilesTool(fs),\n        SearchFilesTool(fs),\n        ReadFileTool(fs),\n        WriteFileTool(fs),\n        EditFileTool(fs),\n    ]\n"
  },
  {
    "path": "src/vanna/tools/python.py",
    "content": "\"\"\"Python-specific tooling built on top of the file system service.\"\"\"\n\nfrom __future__ import annotations\n\nimport shlex\nimport sys\nfrom typing import Any, List, Optional, Sequence, Type\n\nfrom pydantic import BaseModel, Field\n\nfrom vanna.components import (\n    UiComponent,\n    CardComponent,\n    ComponentType,\n    NotificationComponent,\n    SimpleTextComponent,\n)\nfrom vanna.core.tool import Tool, ToolContext, ToolResult\n\nfrom .file_system import CommandResult, FileSystem, LocalFileSystem\n\nMAX_OUTPUT_LENGTH = 4000\n\n\nclass RunPythonFileArgs(BaseModel):\n    \"\"\"Arguments required to execute a Python file.\"\"\"\n\n    filename: str = Field(\n        description=\"Python file to execute (relative to the workspace root)\"\n    )\n    arguments: Sequence[str] = Field(\n        default_factory=list,\n        description=\"Optional arguments to pass to the Python script\",\n    )\n    timeout_seconds: Optional[float] = Field(\n        default=None,\n        ge=0,\n        description=\"Optional timeout for the command in seconds\",\n    )\n\n\nclass RunPythonFileTool(Tool[RunPythonFileArgs]):\n    \"\"\"Execute a Python file using the provided file system service.\"\"\"\n\n    def __init__(self, file_system: Optional[FileSystem] = None):\n        self.file_system = file_system or LocalFileSystem()\n\n    @property\n    def name(self) -> str:\n        return \"run_python_file\"\n\n    @property\n    def description(self) -> str:\n        return \"Execute a Python file using the workspace interpreter\"\n\n    def get_args_schema(self) -> Type[RunPythonFileArgs]:\n        return RunPythonFileArgs\n\n    async def execute(\n        self, context: ToolContext, args: RunPythonFileArgs\n    ) -> ToolResult:\n        exists = await self.file_system.exists(args.filename, context)\n        if not exists:\n            message = f\"Cannot execute '{args.filename}' because it does not exist.\"\n            return _error_result(message)\n\n        command_parts = [sys.executable, args.filename]\n        command_parts.extend(args.arguments)\n        command = _quote_command(command_parts)\n\n        try:\n            result = await self.file_system.run_bash(\n                command,\n                context,\n                timeout=args.timeout_seconds,\n            )\n        except TimeoutError as exc:\n            message = str(exc)\n            return _error_result(message)\n\n        summary = f\"Executed python {args.filename} (exit code {result.returncode}).\"\n        success = result.returncode == 0\n        return _result_from_command(summary, command, result, success=success)\n\n\nclass PipInstallArgs(BaseModel):\n    \"\"\"Arguments required to run pip install.\"\"\"\n\n    packages: List[str] = Field(\n        description=\"Packages (with optional specifiers) to install\", min_length=1\n    )\n    upgrade: bool = Field(\n        default=False,\n        description=\"Whether to include --upgrade in the pip invocation\",\n    )\n    extra_args: Sequence[str] = Field(\n        default_factory=list,\n        description=\"Additional arguments to pass to pip install\",\n    )\n    timeout_seconds: Optional[float] = Field(\n        default=None,\n        ge=0,\n        description=\"Optional timeout for the command in seconds\",\n    )\n\n\nclass PipInstallTool(Tool[PipInstallArgs]):\n    \"\"\"Install Python packages using pip inside the workspace environment.\"\"\"\n\n    def __init__(self, file_system: Optional[FileSystem] = None):\n        self.file_system = file_system or LocalFileSystem()\n\n    @property\n    def name(self) -> str:\n        return \"pip_install\"\n\n    @property\n    def description(self) -> str:\n        return \"Install Python packages using pip\"\n\n    def get_args_schema(self) -> Type[PipInstallArgs]:\n        return PipInstallArgs\n\n    async def execute(self, context: ToolContext, args: PipInstallArgs) -> ToolResult:\n        command_parts = [sys.executable, \"-m\", \"pip\", \"install\"]\n        if args.upgrade:\n            command_parts.append(\"--upgrade\")\n        command_parts.extend(args.packages)\n        command_parts.extend(args.extra_args)\n        command = _quote_command(command_parts)\n\n        try:\n            result = await self.file_system.run_bash(\n                command,\n                context,\n                timeout=args.timeout_seconds,\n            )\n        except TimeoutError as exc:\n            return _error_result(str(exc))\n\n        success = result.returncode == 0\n        summary = (\n            \"pip install completed successfully\"\n            if success\n            else f\"pip install failed (exit code {result.returncode}).\"\n        )\n\n        return _result_from_command(summary, command, result, success=success)\n\n\ndef create_python_tools(file_system: Optional[FileSystem] = None) -> List[Tool[Any]]:\n    \"\"\"Create Python-specific tools backed by a shared file system service.\"\"\"\n\n    fs = file_system or LocalFileSystem()\n    return [\n        RunPythonFileTool(fs),\n        PipInstallTool(fs),\n    ]\n\n\ndef _quote_command(parts: Sequence[str]) -> str:\n    return \" \".join(shlex.quote(part) for part in parts)\n\n\ndef _truncate(text: str, limit: int = MAX_OUTPUT_LENGTH) -> str:\n    if len(text) <= limit:\n        return text\n    return f\"{text[: limit - 1]}…\"\n\n\ndef _result_from_command(\n    summary: str,\n    command: str,\n    result: CommandResult,\n    *,\n    success: bool = True,\n) -> ToolResult:\n    stdout = result.stdout.strip()\n    stderr = result.stderr.strip()\n\n    blocks: List[str] = [f\"$ {command}\"]\n    if stdout:\n        blocks.append(\"STDOUT:\\n\" + _truncate(stdout))\n    if stderr:\n        blocks.append(\"STDERR:\\n\" + _truncate(stderr))\n    if not stdout and not stderr:\n        blocks.append(\"(no output)\")\n\n    content = \"\\n\\n\".join(blocks)\n    card_status = \"success\" if success else \"error\"\n    component = CardComponent(\n        type=ComponentType.CARD,\n        title=\"Command Result\",\n        content=content,\n        status=card_status,\n    )\n\n    return ToolResult(\n        success=success,\n        result_for_llm=f\"{summary}\\n\\n{content}\",\n        ui_component=UiComponent(\n            rich_component=component,\n            simple_component=SimpleTextComponent(text=summary),\n        ),\n        error=None if success else content,\n    )\n\n\ndef _error_result(message: str) -> ToolResult:\n    return ToolResult(\n        success=False,\n        result_for_llm=message,\n        ui_component=UiComponent(\n            rich_component=NotificationComponent(\n                type=ComponentType.NOTIFICATION,\n                level=\"error\",\n                message=message,\n            ),\n            simple_component=SimpleTextComponent(text=message),\n        ),\n        error=message,\n    )\n"
  },
  {
    "path": "src/vanna/tools/run_sql.py",
    "content": "\"\"\"Generic SQL query execution tool with dependency injection.\"\"\"\n\nfrom typing import Any, Dict, List, Optional, Type, cast\nimport uuid\nfrom vanna.core.tool import Tool, ToolContext, ToolResult\nfrom vanna.components import (\n    UiComponent,\n    DataFrameComponent,\n    NotificationComponent,\n    ComponentType,\n    SimpleTextComponent,\n)\nfrom vanna.capabilities.sql_runner import SqlRunner, RunSqlToolArgs\nfrom vanna.capabilities.file_system import FileSystem\nfrom vanna.integrations.local import LocalFileSystem\n\n\nclass RunSqlTool(Tool[RunSqlToolArgs]):\n    \"\"\"Tool that executes SQL queries using an injected SqlRunner implementation.\"\"\"\n\n    def __init__(\n        self,\n        sql_runner: SqlRunner,\n        file_system: Optional[FileSystem] = None,\n        custom_tool_name: Optional[str] = None,\n        custom_tool_description: Optional[str] = None,\n    ):\n        \"\"\"Initialize the tool with a SqlRunner implementation.\n\n        Args:\n            sql_runner: SqlRunner implementation that handles actual query execution\n            file_system: FileSystem implementation for saving results (defaults to LocalFileSystem)\n            custom_tool_name: Optional custom name for the tool (overrides default \"run_sql\")\n            custom_tool_description: Optional custom description for the tool (overrides default description)\n        \"\"\"\n        self.sql_runner = sql_runner\n        self.file_system = file_system or LocalFileSystem()\n        self._custom_name = custom_tool_name\n        self._custom_description = custom_tool_description\n\n    @property\n    def name(self) -> str:\n        return self._custom_name if self._custom_name else \"run_sql\"\n\n    @property\n    def description(self) -> str:\n        return (\n            self._custom_description\n            if self._custom_description\n            else \"Execute SQL queries against the configured database\"\n        )\n\n    def get_args_schema(self) -> Type[RunSqlToolArgs]:\n        return RunSqlToolArgs\n\n    async def execute(self, context: ToolContext, args: RunSqlToolArgs) -> ToolResult:\n        \"\"\"Execute a SQL query using the injected SqlRunner.\"\"\"\n        try:\n            # Use the injected SqlRunner to execute the query\n            df = await self.sql_runner.run_sql(args, context)\n\n            # Determine query type\n            query_type = args.sql.strip().upper().split()[0]\n\n            if query_type == \"SELECT\":\n                # Handle SELECT queries with results\n                if df.empty:\n                    result = \"Query executed successfully. No rows returned.\"\n                    ui_component = UiComponent(\n                        rich_component=DataFrameComponent(\n                            rows=[],\n                            columns=[],\n                            title=\"Query Results\",\n                            description=\"No rows returned\",\n                        ),\n                        simple_component=SimpleTextComponent(text=result),\n                    )\n                    metadata = {\n                        \"row_count\": 0,\n                        \"columns\": [],\n                        \"query_type\": query_type,\n                        \"results\": [],\n                    }\n                else:\n                    # Convert DataFrame to records\n                    results_data = df.to_dict(\"records\")\n                    columns = df.columns.tolist()\n                    row_count = len(df)\n\n                    # Write DataFrame to CSV file for downstream tools\n                    file_id = str(uuid.uuid4())[:8]\n                    filename = f\"query_results_{file_id}.csv\"\n                    csv_content = df.to_csv(index=False)\n                    await self.file_system.write_file(\n                        filename, csv_content, context, overwrite=True\n                    )\n\n                    # Create result text for LLM with truncated results\n                    results_preview = csv_content\n                    if len(results_preview) > 1000:\n                        results_preview = (\n                            results_preview[:1000]\n                            + \"\\n(Results truncated to 1000 characters. FOR LARGE RESULTS YOU DO NOT NEED TO SUMMARIZE THESE RESULTS OR PROVIDE OBSERVATIONS. THE NEXT STEP SHOULD BE A VISUALIZE_DATA CALL)\"\n                        )\n\n                    result = f\"{results_preview}\\n\\nResults saved to file: {filename}\\n\\n**IMPORTANT: FOR VISUALIZE_DATA USE FILENAME: {filename}**\"\n\n                    # Create DataFrame component for UI\n                    dataframe_component = DataFrameComponent.from_records(\n                        records=cast(List[Dict[str, Any]], results_data),\n                        title=\"Query Results\",\n                        description=f\"SQL query returned {row_count} rows with {len(columns)} columns\",\n                    )\n\n                    ui_component = UiComponent(\n                        rich_component=dataframe_component,\n                        simple_component=SimpleTextComponent(text=result),\n                    )\n\n                    metadata = {\n                        \"row_count\": row_count,\n                        \"columns\": columns,\n                        \"query_type\": query_type,\n                        \"results\": results_data,\n                        \"output_file\": filename,\n                    }\n            else:\n                # For non-SELECT queries (INSERT, UPDATE, DELETE, etc.)\n                # The SqlRunner should return a DataFrame with affected row count\n                rows_affected = len(df) if not df.empty else 0\n                result = (\n                    f\"Query executed successfully. {rows_affected} row(s) affected.\"\n                )\n\n                metadata = {\"rows_affected\": rows_affected, \"query_type\": query_type}\n                ui_component = UiComponent(\n                    rich_component=NotificationComponent(\n                        type=ComponentType.NOTIFICATION, level=\"success\", message=result\n                    ),\n                    simple_component=SimpleTextComponent(text=result),\n                )\n\n            return ToolResult(\n                success=True,\n                result_for_llm=result,\n                ui_component=ui_component,\n                metadata=metadata,\n            )\n\n        except Exception as e:\n            error_message = f\"Error executing query: {str(e)}\"\n            return ToolResult(\n                success=False,\n                result_for_llm=error_message,\n                ui_component=UiComponent(\n                    rich_component=NotificationComponent(\n                        type=ComponentType.NOTIFICATION,\n                        level=\"error\",\n                        message=error_message,\n                    ),\n                    simple_component=SimpleTextComponent(text=error_message),\n                ),\n                error=str(e),\n                metadata={\"error_type\": \"sql_error\"},\n            )\n"
  },
  {
    "path": "src/vanna/tools/visualize_data.py",
    "content": "\"\"\"Tool for visualizing DataFrame data from CSV files.\"\"\"\n\nfrom typing import Optional, Type\nimport logging\nimport pandas as pd\nfrom pydantic import BaseModel, Field\n\nfrom vanna.core.tool import Tool, ToolContext, ToolResult\nfrom vanna.components import (\n    UiComponent,\n    ChartComponent,\n    NotificationComponent,\n    ComponentType,\n    SimpleTextComponent,\n)\n\nfrom .file_system import FileSystem, LocalFileSystem\nfrom vanna.integrations.plotly import PlotlyChartGenerator\n\nlogger = logging.getLogger(__name__)\n\n\nclass VisualizeDataArgs(BaseModel):\n    \"\"\"Arguments for visualize_data tool.\"\"\"\n\n    filename: str = Field(description=\"Name of the CSV file to visualize\")\n    title: Optional[str] = Field(\n        default=None, description=\"Optional title for the chart\"\n    )\n\n\nclass VisualizeDataTool(Tool[VisualizeDataArgs]):\n    \"\"\"Tool that reads CSV files and generates visualizations using dependency injection.\"\"\"\n\n    def __init__(\n        self,\n        file_system: Optional[FileSystem] = None,\n        plotly_generator: Optional[PlotlyChartGenerator] = None,\n    ):\n        \"\"\"Initialize the tool with FileSystem and PlotlyChartGenerator.\n\n        Args:\n            file_system: FileSystem implementation for reading CSV files (defaults to LocalFileSystem)\n            plotly_generator: PlotlyChartGenerator for creating Plotly charts (defaults to PlotlyChartGenerator())\n        \"\"\"\n        self.file_system = file_system or LocalFileSystem()\n        self.plotly_generator = plotly_generator or PlotlyChartGenerator()\n\n    @property\n    def name(self) -> str:\n        return \"visualize_data\"\n\n    @property\n    def description(self) -> str:\n        return \"Create a visualization from a CSV file. The tool automatically selects an appropriate chart type based on the data.\"\n\n    def get_args_schema(self) -> Type[VisualizeDataArgs]:\n        return VisualizeDataArgs\n\n    async def execute(\n        self, context: ToolContext, args: VisualizeDataArgs\n    ) -> ToolResult:\n        \"\"\"Read CSV file and generate visualization.\"\"\"\n        try:\n            logger.info(f\"Starting visualization for file: {args.filename}\")\n\n            # Read the CSV file using FileSystem\n            csv_content = await self.file_system.read_file(args.filename, context)\n            logger.info(f\"Read {len(csv_content)} bytes from CSV file\")\n\n            # Parse CSV into DataFrame\n            import io\n\n            df = pd.read_csv(io.StringIO(csv_content))\n            logger.info(\n                f\"Parsed DataFrame with shape {df.shape}, columns: {df.columns.tolist()}, dtypes: {df.dtypes.to_dict()}\"\n            )\n\n            # Generate title\n            title = args.title or f\"Visualization of {args.filename}\"\n\n            # Generate chart using PlotlyChartGenerator\n            logger.info(\"Generating chart...\")\n            chart_dict = self.plotly_generator.generate_chart(df, title)\n            logger.info(\n                f\"Chart generated, type: {type(chart_dict)}, keys: {list(chart_dict.keys()) if isinstance(chart_dict, dict) else 'N/A'}\"\n            )\n\n            # Create result message\n            row_count = len(df)\n            col_count = len(df.columns)\n            result = f\"Created visualization from '{args.filename}' ({row_count} rows, {col_count} columns).\"\n\n            # Create ChartComponent\n            logger.info(\"Creating ChartComponent...\")\n            chart_component = ChartComponent(\n                chart_type=\"plotly\",\n                data=chart_dict,\n                title=title,\n                config={\n                    \"data_shape\": {\"rows\": row_count, \"columns\": col_count},\n                    \"source_file\": args.filename,\n                },\n            )\n            logger.info(\"ChartComponent created successfully\")\n\n            logger.info(\"Creating ToolResult...\")\n            tool_result = ToolResult(\n                success=True,\n                result_for_llm=result,\n                ui_component=UiComponent(\n                    rich_component=chart_component,\n                    simple_component=SimpleTextComponent(text=result),\n                ),\n                metadata={\n                    \"filename\": args.filename,\n                    \"rows\": row_count,\n                    \"columns\": col_count,\n                    \"chart\": chart_dict,\n                },\n            )\n            logger.info(\"ToolResult created successfully\")\n            return tool_result\n\n        except FileNotFoundError as e:\n            logger.error(f\"File not found: {args.filename}\", exc_info=True)\n            error_message = f\"File not found: {args.filename}\"\n            return ToolResult(\n                success=False,\n                result_for_llm=error_message,\n                ui_component=UiComponent(\n                    rich_component=NotificationComponent(\n                        type=ComponentType.NOTIFICATION,\n                        level=\"error\",\n                        message=error_message,\n                    ),\n                    simple_component=SimpleTextComponent(text=error_message),\n                ),\n                error=str(e),\n                metadata={\"error_type\": \"file_not_found\"},\n            )\n        except pd.errors.ParserError as e:\n            logger.error(f\"CSV parse error for {args.filename}\", exc_info=True)\n            error_message = f\"Failed to parse CSV file '{args.filename}': {str(e)}\"\n            return ToolResult(\n                success=False,\n                result_for_llm=error_message,\n                ui_component=UiComponent(\n                    rich_component=NotificationComponent(\n                        type=ComponentType.NOTIFICATION,\n                        level=\"error\",\n                        message=error_message,\n                    ),\n                    simple_component=SimpleTextComponent(text=error_message),\n                ),\n                error=str(e),\n                metadata={\"error_type\": \"csv_parse_error\"},\n            )\n        except ValueError as e:\n            logger.error(f\"Visualization error for {args.filename}\", exc_info=True)\n            error_message = f\"Cannot visualize data: {str(e)}\"\n            return ToolResult(\n                success=False,\n                result_for_llm=error_message,\n                ui_component=UiComponent(\n                    rich_component=NotificationComponent(\n                        type=ComponentType.NOTIFICATION,\n                        level=\"error\",\n                        message=error_message,\n                    ),\n                    simple_component=SimpleTextComponent(text=error_message),\n                ),\n                error=str(e),\n                metadata={\"error_type\": \"visualization_error\"},\n            )\n        except Exception as e:\n            logger.error(\n                f\"Unexpected error creating visualization for {args.filename}\",\n                exc_info=True,\n            )\n            error_message = f\"Error creating visualization: {str(e)}\"\n            return ToolResult(\n                success=False,\n                result_for_llm=error_message,\n                ui_component=UiComponent(\n                    rich_component=NotificationComponent(\n                        type=ComponentType.NOTIFICATION,\n                        level=\"error\",\n                        message=error_message,\n                    ),\n                    simple_component=SimpleTextComponent(text=error_message),\n                ),\n                error=str(e),\n                metadata={\"error_type\": \"general_error\"},\n            )\n"
  },
  {
    "path": "src/vanna/utils/__init__.py",
    "content": ""
  },
  {
    "path": "src/vanna/web_components/__init__.py",
    "content": "\"\"\"\nWeb components for Vanna Agents.\n\nThis module provides web components built with Lit that can be embedded\nin web applications to provide rich UI for Vanna agent interactions.\n\"\"\"\n\nimport os\nfrom pathlib import Path\nfrom typing import Dict\n\n\ndef get_component_files() -> Dict[str, Path]:\n    \"\"\"Get paths to all web component files.\"\"\"\n    component_dir = Path(__file__).parent\n    return {\n        \"js\": component_dir / \"index.js\",\n        \"css\": component_dir / \"style.css\",\n    }\n\n\ndef get_component_html() -> str:\n    \"\"\"Get HTML template for including components.\"\"\"\n    files = get_component_files()\n\n    html = \"\"\"\n<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <title>Vanna AI Chat</title>\n</head>\n<body>\n    <vanna-chat title=\"Vanna AI Assistant\"></vanna-chat>\n    <script type=\"module\" src=\"{js_file}\"></script>\n</body>\n</html>\n\"\"\".format(js_file=files[\"js\"].name)\n\n    return html\n\n\n__all__ = [\"get_component_files\", \"get_component_html\"]\n"
  },
  {
    "path": "tests/conftest.py",
    "content": "\"\"\"\nPytest configuration and shared fixtures for Vanna v2 test suite.\n\"\"\"\n\nimport os\nimport pytest\nimport sqlite3\nfrom pathlib import Path\n\n# Configure pytest-asyncio\npytest_plugins = [\"pytest_asyncio\"]\n\n\ndef pytest_configure(config):\n    \"\"\"Configure pytest with custom markers.\"\"\"\n    config.addinivalue_line(\n        \"markers\", \"anthropic: marks tests requiring Anthropic API key\"\n    )\n    config.addinivalue_line(\"markers\", \"openai: marks tests requiring OpenAI API key\")\n    config.addinivalue_line(\n        \"markers\", \"azureopenai: marks tests requiring Azure OpenAI API key\"\n    )\n    config.addinivalue_line(\"markers\", \"gemini: marks tests requiring Google API key\")\n    config.addinivalue_line(\"markers\", \"ollama: marks tests requiring Ollama\")\n    config.addinivalue_line(\"markers\", \"chromadb: marks tests requiring ChromaDB\")\n    config.addinivalue_line(\"markers\", \"legacy: marks tests for LegacyVannaAdapter\")\n\n\ndef pytest_collection_modifyitems(config, items):\n    \"\"\"Automatically skip tests if required API keys are missing.\"\"\"\n    for item in items:\n        # Skip Anthropic tests if no API key\n        if \"anthropic\" in item.keywords:\n            if not os.getenv(\"ANTHROPIC_API_KEY\"):\n                item.add_marker(\n                    pytest.mark.skip(\n                        reason=\"ANTHROPIC_API_KEY environment variable not set\"\n                    )\n                )\n\n        # Skip OpenAI tests if no API key\n        if \"openai\" in item.keywords:\n            if not os.getenv(\"OPENAI_API_KEY\"):\n                item.add_marker(\n                    pytest.mark.skip(\n                        reason=\"OPENAI_API_KEY environment variable not set\"\n                    )\n                )\n\n        # Skip Azure OpenAI tests if no API key\n        if \"azureopenai\" in item.keywords:\n            if not os.getenv(\"AZURE_OPENAI_API_KEY\"):\n                item.add_marker(\n                    pytest.mark.skip(\n                        reason=\"AZURE_OPENAI_API_KEY environment variable not set\"\n                    )\n                )\n\n        # Skip Gemini tests if no API key\n        if \"gemini\" in item.keywords:\n            if not (os.getenv(\"GOOGLE_API_KEY\") or os.getenv(\"GEMINI_API_KEY\")):\n                item.add_marker(\n                    pytest.mark.skip(\n                        reason=\"GOOGLE_API_KEY or GEMINI_API_KEY environment variable not set\"\n                    )\n                )\n\n\n@pytest.fixture(scope=\"session\")\ndef chinook_db(tmp_path_factory):\n    \"\"\"\n    Downloads the Chinook SQLite database and returns a SqliteRunner.\n\n    Uses session scope so the database is only downloaded once per test session.\n    \"\"\"\n    import httpx\n    from vanna.integrations.sqlite import SqliteRunner\n\n    tmp_path = tmp_path_factory.mktemp(\"data\")\n    db_path = tmp_path / \"Chinook.sqlite\"\n\n    # Download the database\n    response = httpx.get(\"https://vanna.ai/Chinook.sqlite\")\n    response.raise_for_status()\n\n    db_path.write_bytes(response.content)\n\n    return SqliteRunner(database_path=str(db_path))\n"
  },
  {
    "path": "tests/test_agent_memory.py",
    "content": "\"\"\"\nIntegration tests for AgentMemory implementations.\n\nThese tests verify actual functionality against running services.\nTests are marked by implementation and skip if dependencies are unavailable.\n\"\"\"\n\nimport pytest\nimport asyncio\nimport tempfile\nimport shutil\nimport os\nfrom vanna.core.user import User\nfrom vanna.core.tool import ToolContext\nfrom vanna.integrations.local.agent_memory import DemoAgentMemory\n\n\n@pytest.fixture\ndef test_user():\n    \"\"\"Test user for context.\"\"\"\n    return User(\n        id=\"test_user\",\n        username=\"test\",\n        email=\"test@example.com\",\n        group_memberships=[\"user\"],\n    )\n\n\ndef create_test_context(test_user, agent_memory):\n    \"\"\"Helper to create test context with specific agent memory.\"\"\"\n    return ToolContext(\n        user=test_user,\n        conversation_id=\"test_conv\",\n        request_id=\"test_req\",\n        agent_memory=agent_memory,\n        metadata={},\n    )\n\n\n@pytest.fixture\ndef chromadb_memory():\n    \"\"\"Create ChromaDB memory instance.\"\"\"\n    try:\n        from vanna.integrations.chromadb import ChromaAgentMemory\n\n        temp_dir = tempfile.mkdtemp()\n        memory = ChromaAgentMemory(\n            persist_directory=temp_dir, collection_name=\"test_memories\"\n        )\n\n        yield memory\n\n        shutil.rmtree(temp_dir, ignore_errors=True)\n    except ImportError:\n        pytest.skip(\"ChromaDB not installed\")\n\n\n@pytest.fixture\ndef qdrant_memory():\n    \"\"\"Create Qdrant memory instance.\"\"\"\n    try:\n        from vanna.integrations.qdrant import QdrantAgentMemory\n\n        temp_dir = tempfile.mkdtemp()\n        memory = QdrantAgentMemory(path=temp_dir)\n\n        yield memory\n\n        shutil.rmtree(temp_dir, ignore_errors=True)\n    except ImportError:\n        pytest.skip(\"Qdrant not installed\")\n\n\n@pytest.fixture\ndef faiss_memory():\n    \"\"\"Create FAISS memory instance.\"\"\"\n    try:\n        from vanna.integrations.faiss import FAISSAgentMemory\n\n        temp_dir = tempfile.mkdtemp()\n        memory = FAISSAgentMemory(persist_path=temp_dir)\n\n        yield memory\n\n        shutil.rmtree(temp_dir, ignore_errors=True)\n    except ImportError:\n        pytest.skip(\"FAISS not installed\")\n\n\n# Parametrized tests for local implementations\n@pytest.mark.parametrize(\n    \"memory_fixture\", [\"chromadb_memory\", \"qdrant_memory\", \"faiss_memory\"]\n)\nclass TestLocalAgentMemory:\n    \"\"\"Tests for local AgentMemory implementations (ChromaDB, Qdrant, FAISS).\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_save_and_search(self, memory_fixture, test_user, request):\n        \"\"\"Test saving and searching tool usage patterns.\"\"\"\n        memory = request.getfixturevalue(memory_fixture)\n        test_context = create_test_context(test_user, memory)\n\n        await memory.clear_memories(context=test_context)\n\n        await memory.save_tool_usage(\n            question=\"Show me top customers\",\n            tool_name=\"run_sql\",\n            args={\"sql\": \"SELECT * FROM customers ORDER BY total_spent DESC LIMIT 10\"},\n            context=test_context,\n            success=True,\n        )\n\n        results = await memory.search_similar_usage(\n            question=\"Show me best customers\",\n            context=test_context,\n            limit=5,\n            similarity_threshold=0.5,\n        )\n\n        assert len(results) > 0\n        assert results[0].memory.question == \"Show me top customers\"\n        assert results[0].memory.tool_name == \"run_sql\"\n        assert results[0].similarity_score > 0.5\n\n    @pytest.mark.asyncio\n    async def test_multiple_memories(self, memory_fixture, test_user, request):\n        \"\"\"Test storing and searching multiple memories.\"\"\"\n        memory = request.getfixturevalue(memory_fixture)\n        test_context = create_test_context(test_user, memory)\n\n        await memory.clear_memories(context=test_context)\n\n        patterns = [\n            (\"Show me sales data\", \"run_sql\", {\"sql\": \"SELECT * FROM sales\"}),\n            (\"Get customer list\", \"run_sql\", {\"sql\": \"SELECT * FROM customers\"}),\n            (\n                \"Generate sales report\",\n                \"run_sql\",\n                {\"sql\": \"SELECT date, SUM(amount) FROM sales GROUP BY date\"},\n            ),\n        ]\n\n        for question, tool_name, args in patterns:\n            await memory.save_tool_usage(\n                question=question,\n                tool_name=tool_name,\n                args=args,\n                context=test_context,\n                success=True,\n            )\n\n        results = await memory.search_similar_usage(\n            question=\"Show me sales information\",\n            context=test_context,\n            limit=10,\n            similarity_threshold=0.3,\n        )\n\n        assert len(results) >= 2\n\n    @pytest.mark.asyncio\n    async def test_tool_filter(self, memory_fixture, test_user, request):\n        \"\"\"Test filtering by tool name.\"\"\"\n        memory = request.getfixturevalue(memory_fixture)\n        test_context = create_test_context(test_user, memory)\n\n        await memory.clear_memories(context=test_context)\n\n        await memory.save_tool_usage(\n            question=\"Query database\",\n            tool_name=\"run_sql\",\n            args={\"sql\": \"SELECT 1\"},\n            context=test_context,\n        )\n\n        await memory.save_tool_usage(\n            question=\"Search files\",\n            tool_name=\"search_files\",\n            args={\"pattern\": \"*.py\"},\n            context=test_context,\n        )\n\n        results = await memory.search_similar_usage(\n            question=\"Query data\",\n            context=test_context,\n            tool_name_filter=\"run_sql\",\n            similarity_threshold=0.0,\n        )\n\n        assert len(results) > 0\n        assert all(r.memory.tool_name == \"run_sql\" for r in results)\n\n    @pytest.mark.asyncio\n    async def test_clear_memories(self, memory_fixture, test_user, request):\n        \"\"\"Test clearing memories.\"\"\"\n        memory = request.getfixturevalue(memory_fixture)\n        test_context = create_test_context(test_user, memory)\n\n        await memory.clear_memories(context=test_context)\n\n        await memory.save_tool_usage(\"Q1\", \"run_sql\", {}, test_context)\n        await memory.save_tool_usage(\"Q2\", \"run_sql\", {}, test_context)\n        await memory.save_tool_usage(\"Q3\", \"visualize_data\", {}, test_context)\n\n        deleted = await memory.clear_memories(context=test_context, tool_name=\"run_sql\")\n\n        assert deleted >= 0\n\n    @pytest.mark.asyncio\n    async def test_get_recent_memories(self, memory_fixture, test_user, request):\n        \"\"\"Test getting recent memories.\"\"\"\n        memory = request.getfixturevalue(memory_fixture)\n        test_context = create_test_context(test_user, memory)\n\n        await memory.clear_memories(context=test_context)\n\n        await memory.save_tool_usage(\"Q1\", \"run_sql\", {\"sql\": \"SELECT 1\"}, test_context)\n        await asyncio.sleep(0.01)\n        await memory.save_tool_usage(\"Q2\", \"run_sql\", {\"sql\": \"SELECT 2\"}, test_context)\n        await asyncio.sleep(0.01)\n        await memory.save_tool_usage(\n            \"Q3\", \"visualize_data\", {\"file\": \"data.csv\"}, test_context\n        )\n        await asyncio.sleep(0.01)\n        await memory.save_tool_usage(\"Q4\", \"run_sql\", {\"sql\": \"SELECT 3\"}, test_context)\n        await asyncio.sleep(0.01)\n        await memory.save_tool_usage(\n            \"Q5\", \"search_files\", {\"pattern\": \"*.py\"}, test_context\n        )\n\n        recent = await memory.get_recent_memories(context=test_context, limit=3)\n\n        assert isinstance(recent, list)\n        assert len(recent) <= 3\n\n        if recent:\n            assert all(hasattr(m, \"memory_id\") for m in recent)\n\n    @pytest.mark.asyncio\n    async def test_delete_by_id(self, memory_fixture, test_user, request):\n        \"\"\"Test deleting memories by ID.\"\"\"\n        memory = request.getfixturevalue(memory_fixture)\n        test_context = create_test_context(test_user, memory)\n\n        await memory.clear_memories(context=test_context)\n\n        await memory.save_tool_usage(\"Q1\", \"run_sql\", {\"sql\": \"SELECT 1\"}, test_context)\n        await memory.save_tool_usage(\"Q2\", \"run_sql\", {\"sql\": \"SELECT 2\"}, test_context)\n        await memory.save_tool_usage(\n            \"Q3\", \"visualize_data\", {\"file\": \"data.csv\"}, test_context\n        )\n\n        recent = await memory.get_recent_memories(context=test_context, limit=10)\n\n        if not recent:\n            pytest.skip(\"Implementation doesn't support get_recent_memories\")\n\n        assert all(m.memory_id is not None for m in recent)\n\n        memory_to_delete = recent[0]\n        deleted = await memory.delete_by_id(\n            context=test_context, memory_id=memory_to_delete.memory_id\n        )\n\n        assert deleted is True\n\n        remaining = await memory.get_recent_memories(context=test_context, limit=10)\n        assert all(m.memory_id != memory_to_delete.memory_id for m in remaining)\n\n        fake_deleted = await memory.delete_by_id(\n            context=test_context, memory_id=\"fake-id-12345\"\n        )\n        assert fake_deleted is False\n\n    @pytest.mark.asyncio\n    async def test_save_and_search_text_memories(\n        self, memory_fixture, test_user, request\n    ):\n        \"\"\"Test saving and searching text memories.\"\"\"\n        memory = request.getfixturevalue(memory_fixture)\n        test_context = create_test_context(test_user, memory)\n\n        await memory.clear_memories(context=test_context)\n\n        # Save a text memory\n        text_memory = await memory.save_text_memory(\n            content=\"The status column uses 1 for active, 0 for inactive\",\n            context=test_context,\n        )\n\n        assert text_memory.memory_id is not None\n        assert (\n            text_memory.content == \"The status column uses 1 for active, 0 for inactive\"\n        )\n\n        # Search for similar text memories\n        results = await memory.search_text_memories(\n            query=\"status column meaning\",\n            context=test_context,\n            limit=5,\n            similarity_threshold=0.3,\n        )\n\n        assert len(results) > 0\n        assert (\n            results[0].memory.content\n            == \"The status column uses 1 for active, 0 for inactive\"\n        )\n        assert results[0].similarity_score > 0.3\n\n    @pytest.mark.asyncio\n    async def test_multiple_text_memories(self, memory_fixture, test_user, request):\n        \"\"\"Test storing and searching multiple text memories.\"\"\"\n        memory = request.getfixturevalue(memory_fixture)\n        test_context = create_test_context(test_user, memory)\n\n        await memory.clear_memories(context=test_context)\n\n        text_contents = [\n            \"The fiscal year starts in April\",\n            \"MRR means Monthly Recurring Revenue\",\n            \"Always exclude test accounts where email contains 'test'\",\n        ]\n\n        for content in text_contents:\n            await memory.save_text_memory(content=content, context=test_context)\n\n        # Search for fiscal year info\n        results = await memory.search_text_memories(\n            query=\"When does the fiscal year start?\",\n            context=test_context,\n            limit=10,\n            similarity_threshold=0.2,\n        )\n\n        assert len(results) >= 1\n        assert any(\"fiscal year\" in r.memory.content.lower() for r in results)\n\n    @pytest.mark.asyncio\n    async def test_get_recent_text_memories(self, memory_fixture, test_user, request):\n        \"\"\"Test getting recent text memories.\"\"\"\n        memory = request.getfixturevalue(memory_fixture)\n        test_context = create_test_context(test_user, memory)\n\n        await memory.clear_memories(context=test_context)\n\n        await memory.save_text_memory(\"First text memory\", test_context)\n        await asyncio.sleep(0.01)\n        await memory.save_text_memory(\"Second text memory\", test_context)\n        await asyncio.sleep(0.01)\n        await memory.save_text_memory(\"Third text memory\", test_context)\n\n        recent = await memory.get_recent_text_memories(context=test_context, limit=2)\n\n        assert isinstance(recent, list)\n        assert len(recent) <= 2\n\n        if recent:\n            assert all(hasattr(m, \"memory_id\") for m in recent)\n            assert all(hasattr(m, \"content\") for m in recent)\n\n    @pytest.mark.asyncio\n    async def test_delete_text_memory(self, memory_fixture, test_user, request):\n        \"\"\"Test deleting text memories by ID.\"\"\"\n        memory = request.getfixturevalue(memory_fixture)\n        test_context = create_test_context(test_user, memory)\n\n        await memory.clear_memories(context=test_context)\n\n        text_memory = await memory.save_text_memory(\n            \"Test memory to delete\", test_context\n        )\n\n        assert text_memory.memory_id is not None\n\n        deleted = await memory.delete_text_memory(\n            context=test_context, memory_id=text_memory.memory_id\n        )\n\n        assert deleted is True\n\n        # Verify it's gone\n        recent = await memory.get_recent_text_memories(context=test_context, limit=10)\n        assert all(m.memory_id != text_memory.memory_id for m in recent)\n\n        # Try deleting non-existent memory\n        fake_deleted = await memory.delete_text_memory(\n            context=test_context, memory_id=\"fake-text-id-12345\"\n        )\n        assert fake_deleted is False\n\n    @pytest.mark.asyncio\n    async def test_mixed_tool_and_text_memories(\n        self, memory_fixture, test_user, request\n    ):\n        \"\"\"Test that tool memories and text memories can coexist without errors.\"\"\"\n        memory = request.getfixturevalue(memory_fixture)\n        test_context = create_test_context(test_user, memory)\n\n        await memory.clear_memories(context=test_context)\n\n        # Save some tool memories\n        await memory.save_tool_usage(\n            question=\"Show me top customers\",\n            tool_name=\"run_sql\",\n            args={\"sql\": \"SELECT * FROM customers\"},\n            context=test_context,\n        )\n\n        # Save some text memories\n        await memory.save_text_memory(\n            content=\"The fiscal year starts in April\",\n            context=test_context,\n        )\n\n        await memory.save_tool_usage(\n            question=\"Get sales data\",\n            tool_name=\"run_sql\",\n            args={\"sql\": \"SELECT * FROM sales\"},\n            context=test_context,\n        )\n\n        await memory.save_text_memory(\n            content=\"MRR means Monthly Recurring Revenue\",\n            context=test_context,\n        )\n\n        # This should only return tool memories, not text memories\n        tool_memories = await memory.get_recent_memories(context=test_context, limit=10)\n\n        assert isinstance(tool_memories, list)\n        assert len(tool_memories) == 2  # Should only have the 2 tool memories\n        assert all(hasattr(m, \"question\") for m in tool_memories)\n        assert all(hasattr(m, \"tool_name\") for m in tool_memories)\n\n        # This should only return text memories, not tool memories\n        text_memories = await memory.get_recent_text_memories(\n            context=test_context, limit=10\n        )\n\n        assert isinstance(text_memories, list)\n        assert len(text_memories) == 2  # Should only have the 2 text memories\n        assert all(hasattr(m, \"content\") for m in text_memories)\n        assert all(\n            \"fiscal year\" in m.content or \"MRR\" in m.content for m in text_memories\n        )\n"
  },
  {
    "path": "tests/test_agent_memory_sanity.py",
    "content": "\"\"\"\nSanity tests for AgentMemory implementations.\n\nThese tests verify that:\n1. Each AgentMemory implementation correctly implements the AgentMemory interface\n2. Imports are working correctly for all vector store modules\n3. Basic class instantiation works (without requiring actual service connections)\n\nNote: These tests do NOT execute actual vector operations against services.\nThey are lightweight sanity checks for the implementation structure.\n\"\"\"\n\nimport pytest\nfrom inspect import signature, iscoroutinefunction\nfrom abc import ABC\n\n\nclass TestAgentMemoryInterface:\n    \"\"\"Test that the AgentMemory interface is properly defined.\"\"\"\n\n    def test_agent_memory_import(self):\n        \"\"\"Test that AgentMemory can be imported.\"\"\"\n        from vanna.capabilities.agent_memory import AgentMemory\n\n        assert AgentMemory is not None\n\n    def test_agent_memory_is_abstract(self):\n        \"\"\"Test that AgentMemory is an abstract base class.\"\"\"\n        from vanna.capabilities.agent_memory import AgentMemory\n\n        assert issubclass(AgentMemory, ABC)\n\n    def test_agent_memory_has_required_methods(self):\n        \"\"\"Test that AgentMemory defines all required abstract methods.\"\"\"\n        from vanna.capabilities.agent_memory import AgentMemory\n\n        required_methods = [\n            \"save_tool_usage\",\n            \"save_text_memory\",\n            \"search_similar_usage\",\n            \"search_text_memories\",\n            \"get_recent_memories\",\n            \"get_recent_text_memories\",\n            \"delete_by_id\",\n            \"delete_text_memory\",\n            \"clear_memories\",\n        ]\n\n        for method_name in required_methods:\n            assert hasattr(AgentMemory, method_name)\n            method = getattr(AgentMemory, method_name)\n            assert getattr(method, \"__isabstractmethod__\", False), (\n                f\"{method_name} should be abstract\"\n            )\n\n    def test_all_methods_are_async(self):\n        \"\"\"Test that all AgentMemory methods are async.\"\"\"\n        from vanna.capabilities.agent_memory import AgentMemory\n\n        methods = [\n            \"save_tool_usage\",\n            \"save_text_memory\",\n            \"search_similar_usage\",\n            \"search_text_memories\",\n            \"get_recent_memories\",\n            \"get_recent_text_memories\",\n            \"delete_by_id\",\n            \"delete_text_memory\",\n            \"clear_memories\",\n        ]\n\n        for method_name in methods:\n            method = getattr(AgentMemory, method_name)\n            assert iscoroutinefunction(method), f\"{method_name} should be async\"\n\n\nclass TestToolMemoryModel:\n    \"\"\"Test the ToolMemory model.\"\"\"\n\n    def test_tool_memory_import(self):\n        \"\"\"Test that ToolMemory can be imported.\"\"\"\n        from vanna.capabilities.agent_memory import ToolMemory\n\n        assert ToolMemory is not None\n\n    def test_tool_memory_is_pydantic_model(self):\n        \"\"\"Test that ToolMemory is a Pydantic model.\"\"\"\n        from vanna.capabilities.agent_memory import ToolMemory\n        from pydantic import BaseModel\n\n        assert issubclass(ToolMemory, BaseModel)\n\n    def test_tool_memory_has_required_fields(self):\n        \"\"\"Test that ToolMemory has all required fields.\"\"\"\n        from vanna.capabilities.agent_memory import ToolMemory\n\n        required_fields = [\"question\", \"tool_name\", \"args\"]\n        optional_fields = [\"memory_id\", \"timestamp\", \"success\", \"metadata\"]\n\n        model_fields = list(ToolMemory.model_fields.keys())\n\n        for field in required_fields:\n            assert field in model_fields, f\"Required field '{field}' missing\"\n\n        for field in optional_fields:\n            assert field in model_fields, f\"Optional field '{field}' missing\"\n\n    def test_tool_memory_instantiation(self):\n        \"\"\"Test that ToolMemory can be instantiated.\"\"\"\n        from vanna.capabilities.agent_memory import ToolMemory\n\n        memory = ToolMemory(\n            memory_id=\"test-123\",\n            question=\"What is the total sales?\",\n            tool_name=\"run_sql\",\n            args={\"sql\": \"SELECT SUM(amount) FROM sales\"},\n        )\n\n        assert memory.memory_id == \"test-123\"\n        assert memory.question == \"What is the total sales?\"\n        assert memory.tool_name == \"run_sql\"\n        assert memory.args == {\"sql\": \"SELECT SUM(amount) FROM sales\"}\n        assert memory.success is True  # Default value\n\n\nclass TestToolMemorySearchResultModel:\n    \"\"\"Test the ToolMemorySearchResult model.\"\"\"\n\n    def test_memory_search_result_import(self):\n        \"\"\"Test that ToolMemorySearchResult can be imported.\"\"\"\n        from vanna.capabilities.agent_memory import ToolMemorySearchResult\n\n        assert ToolMemorySearchResult is not None\n\n    def test_memory_search_result_instantiation(self):\n        \"\"\"Test that ToolMemorySearchResult can be instantiated.\"\"\"\n        from vanna.capabilities.agent_memory import ToolMemorySearchResult, ToolMemory\n\n        memory = ToolMemory(question=\"test\", tool_name=\"test_tool\", args={})\n\n        result = ToolMemorySearchResult(memory=memory, similarity_score=0.95, rank=1)\n\n        assert result.memory == memory\n        assert result.similarity_score == 0.95\n        assert result.rank == 1\n\n\nclass TestTextMemoryModel:\n    \"\"\"Test the TextMemory model.\"\"\"\n\n    def test_text_memory_import(self):\n        \"\"\"Test that TextMemory can be imported.\"\"\"\n        from vanna.capabilities.agent_memory import TextMemory\n\n        assert TextMemory is not None\n\n    def test_text_memory_is_pydantic_model(self):\n        \"\"\"Test that TextMemory is a Pydantic model.\"\"\"\n        from vanna.capabilities.agent_memory import TextMemory\n        from pydantic import BaseModel\n\n        assert issubclass(TextMemory, BaseModel)\n\n    def test_text_memory_has_required_fields(self):\n        \"\"\"Test that TextMemory has all required fields.\"\"\"\n        from vanna.capabilities.agent_memory import TextMemory\n\n        required_fields = [\"content\"]\n        optional_fields = [\"memory_id\", \"timestamp\"]\n\n        model_fields = list(TextMemory.model_fields.keys())\n\n        for field in required_fields:\n            assert field in model_fields, f\"Required field '{field}' missing\"\n\n        for field in optional_fields:\n            assert field in model_fields, f\"Optional field '{field}' missing\"\n\n    def test_text_memory_instantiation(self):\n        \"\"\"Test that TextMemory can be instantiated.\"\"\"\n        from vanna.capabilities.agent_memory import TextMemory\n\n        memory = TextMemory(\n            memory_id=\"text-123\", content=\"Remember to handle edge cases\"\n        )\n\n        assert memory.memory_id == \"text-123\"\n        assert memory.content == \"Remember to handle edge cases\"\n\n\nclass TestTextMemorySearchResultModel:\n    \"\"\"Test the TextMemorySearchResult model.\"\"\"\n\n    def test_text_memory_search_result_import(self):\n        \"\"\"Test that TextMemorySearchResult can be imported.\"\"\"\n        from vanna.capabilities.agent_memory import TextMemorySearchResult\n\n        assert TextMemorySearchResult is not None\n\n    def test_text_memory_search_result_instantiation(self):\n        \"\"\"Test that TextMemorySearchResult can be instantiated.\"\"\"\n        from vanna.capabilities.agent_memory import TextMemorySearchResult, TextMemory\n\n        memory = TextMemory(content=\"Example memory\")\n\n        result = TextMemorySearchResult(memory=memory, similarity_score=0.88, rank=2)\n\n        assert result.memory == memory\n        assert result.similarity_score == 0.88\n        assert result.rank == 2\n\n\nclass TestChromaDBAgentMemory:\n    \"\"\"Sanity tests for ChromaDB AgentMemory implementation.\"\"\"\n\n    def test_chromadb_import(self):\n        \"\"\"Test that ChromaAgentMemory can be imported.\"\"\"\n        try:\n            from vanna.integrations.chromadb import ChromaAgentMemory\n\n            assert ChromaAgentMemory is not None\n        except ImportError:\n            pytest.skip(\"ChromaDB not installed\")\n\n    def test_chromadb_implements_agent_memory(self):\n        \"\"\"Test that ChromaAgentMemory implements AgentMemory.\"\"\"\n        try:\n            from vanna.integrations.chromadb import ChromaAgentMemory\n            from vanna.capabilities.agent_memory import AgentMemory\n\n            assert issubclass(ChromaAgentMemory, AgentMemory)\n        except ImportError:\n            pytest.skip(\"ChromaDB not installed\")\n\n    def test_chromadb_has_all_methods(self):\n        \"\"\"Test that ChromaAgentMemory implements all required methods.\"\"\"\n        try:\n            from vanna.integrations.chromadb import ChromaAgentMemory\n\n            required_methods = [\n                \"save_tool_usage\",\n                \"save_text_memory\",\n                \"search_similar_usage\",\n                \"search_text_memories\",\n                \"get_recent_memories\",\n                \"get_recent_text_memories\",\n                \"delete_by_id\",\n                \"delete_text_memory\",\n                \"clear_memories\",\n            ]\n\n            for method_name in required_methods:\n                assert hasattr(ChromaAgentMemory, method_name)\n                method = getattr(ChromaAgentMemory, method_name)\n                assert not getattr(method, \"__isabstractmethod__\", False), (\n                    f\"{method_name} should be implemented (not abstract)\"\n                )\n        except ImportError:\n            pytest.skip(\"ChromaDB not installed\")\n\n    def test_chromadb_instantiation(self):\n        \"\"\"Test that ChromaAgentMemory can be instantiated.\"\"\"\n        try:\n            from vanna.integrations.chromadb import ChromaAgentMemory\n            import tempfile\n\n            temp_dir = tempfile.mkdtemp()\n            memory = ChromaAgentMemory(\n                persist_directory=temp_dir, collection_name=\"test\"\n            )\n\n            assert memory is not None\n            assert memory.persist_directory == temp_dir\n            assert memory.collection_name == \"test\"\n        except ImportError:\n            pytest.skip(\"ChromaDB not installed\")\n\n\nclass TestQdrantAgentMemory:\n    \"\"\"Sanity tests for Qdrant AgentMemory implementation.\"\"\"\n\n    def test_qdrant_import(self):\n        \"\"\"Test that QdrantAgentMemory can be imported.\"\"\"\n        try:\n            from vanna.integrations.qdrant import QdrantAgentMemory\n\n            assert QdrantAgentMemory is not None\n        except ImportError:\n            pytest.skip(\"Qdrant not installed\")\n\n    def test_qdrant_implements_agent_memory(self):\n        \"\"\"Test that QdrantAgentMemory implements AgentMemory.\"\"\"\n        try:\n            from vanna.integrations.qdrant import QdrantAgentMemory\n            from vanna.capabilities.agent_memory import AgentMemory\n\n            assert issubclass(QdrantAgentMemory, AgentMemory)\n        except ImportError:\n            pytest.skip(\"Qdrant not installed\")\n\n    def test_qdrant_has_all_methods(self):\n        \"\"\"Test that QdrantAgentMemory implements all required methods.\"\"\"\n        try:\n            from vanna.integrations.qdrant import QdrantAgentMemory\n\n            required_methods = [\n                \"save_tool_usage\",\n                \"save_text_memory\",\n                \"search_similar_usage\",\n                \"search_text_memories\",\n                \"get_recent_memories\",\n                \"get_recent_text_memories\",\n                \"delete_by_id\",\n                \"delete_text_memory\",\n                \"clear_memories\",\n            ]\n\n            for method_name in required_methods:\n                assert hasattr(QdrantAgentMemory, method_name)\n        except ImportError:\n            pytest.skip(\"Qdrant not installed\")\n\n    def test_qdrant_instantiation(self):\n        \"\"\"Test that QdrantAgentMemory can be instantiated.\"\"\"\n        try:\n            from vanna.integrations.qdrant import QdrantAgentMemory\n\n            # In-memory mode doesn't require actual service\n            memory = QdrantAgentMemory(path=\":memory:\")\n\n            assert memory is not None\n        except ImportError:\n            pytest.skip(\"Qdrant not installed\")\n\n\nclass TestPineconeAgentMemory:\n    \"\"\"Sanity tests for Pinecone AgentMemory implementation.\"\"\"\n\n    def test_pinecone_import(self):\n        \"\"\"Test that PineconeAgentMemory can be imported.\"\"\"\n        try:\n            from vanna.integrations.pinecone import PineconeAgentMemory\n\n            assert PineconeAgentMemory is not None\n        except ImportError:\n            pytest.skip(\"Pinecone not installed\")\n\n    def test_pinecone_implements_agent_memory(self):\n        \"\"\"Test that PineconeAgentMemory implements AgentMemory.\"\"\"\n        try:\n            from vanna.integrations.pinecone import PineconeAgentMemory\n            from vanna.capabilities.agent_memory import AgentMemory\n\n            assert issubclass(PineconeAgentMemory, AgentMemory)\n        except ImportError:\n            pytest.skip(\"Pinecone not installed\")\n\n    def test_pinecone_has_all_methods(self):\n        \"\"\"Test that PineconeAgentMemory implements all required methods.\"\"\"\n        try:\n            from vanna.integrations.pinecone import PineconeAgentMemory\n\n            required_methods = [\n                \"save_tool_usage\",\n                \"save_text_memory\",\n                \"search_similar_usage\",\n                \"search_text_memories\",\n                \"get_recent_memories\",\n                \"get_recent_text_memories\",\n                \"delete_by_id\",\n                \"delete_text_memory\",\n                \"clear_memories\",\n            ]\n\n            for method_name in required_methods:\n                assert hasattr(PineconeAgentMemory, method_name)\n        except ImportError:\n            pytest.skip(\"Pinecone not installed\")\n\n\nclass TestMilvusAgentMemory:\n    \"\"\"Sanity tests for Milvus AgentMemory implementation.\"\"\"\n\n    def test_milvus_import(self):\n        \"\"\"Test that MilvusAgentMemory can be imported.\"\"\"\n        try:\n            from vanna.integrations.milvus import MilvusAgentMemory\n\n            assert MilvusAgentMemory is not None\n        except ImportError:\n            pytest.skip(\"Milvus not installed\")\n\n    def test_milvus_implements_agent_memory(self):\n        \"\"\"Test that MilvusAgentMemory implements AgentMemory.\"\"\"\n        try:\n            from vanna.integrations.milvus import MilvusAgentMemory\n            from vanna.capabilities.agent_memory import AgentMemory\n\n            assert issubclass(MilvusAgentMemory, AgentMemory)\n        except ImportError:\n            pytest.skip(\"Milvus not installed\")\n\n    def test_milvus_has_all_methods(self):\n        \"\"\"Test that MilvusAgentMemory implements all required methods.\"\"\"\n        try:\n            from vanna.integrations.milvus import MilvusAgentMemory\n\n            required_methods = [\n                \"save_tool_usage\",\n                \"save_text_memory\",\n                \"search_similar_usage\",\n                \"search_text_memories\",\n                \"get_recent_memories\",\n                \"get_recent_text_memories\",\n                \"delete_by_id\",\n                \"delete_text_memory\",\n                \"clear_memories\",\n            ]\n\n            for method_name in required_methods:\n                assert hasattr(MilvusAgentMemory, method_name)\n        except ImportError:\n            pytest.skip(\"Milvus not installed\")\n\n\nclass TestWeaviateAgentMemory:\n    \"\"\"Sanity tests for Weaviate AgentMemory implementation.\"\"\"\n\n    def test_weaviate_import(self):\n        \"\"\"Test that WeaviateAgentMemory can be imported.\"\"\"\n        try:\n            from vanna.integrations.weaviate import WeaviateAgentMemory\n\n            assert WeaviateAgentMemory is not None\n        except ImportError:\n            pytest.skip(\"Weaviate not installed\")\n\n    def test_weaviate_implements_agent_memory(self):\n        \"\"\"Test that WeaviateAgentMemory implements AgentMemory.\"\"\"\n        try:\n            from vanna.integrations.weaviate import WeaviateAgentMemory\n            from vanna.capabilities.agent_memory import AgentMemory\n\n            assert issubclass(WeaviateAgentMemory, AgentMemory)\n        except ImportError:\n            pytest.skip(\"Weaviate not installed\")\n\n    def test_weaviate_has_all_methods(self):\n        \"\"\"Test that WeaviateAgentMemory implements all required methods.\"\"\"\n        try:\n            from vanna.integrations.weaviate import WeaviateAgentMemory\n\n            required_methods = [\n                \"save_tool_usage\",\n                \"save_text_memory\",\n                \"search_similar_usage\",\n                \"search_text_memories\",\n                \"get_recent_memories\",\n                \"get_recent_text_memories\",\n                \"delete_by_id\",\n                \"delete_text_memory\",\n                \"clear_memories\",\n            ]\n\n            for method_name in required_methods:\n                assert hasattr(WeaviateAgentMemory, method_name)\n        except ImportError:\n            pytest.skip(\"Weaviate not installed\")\n\n\nclass TestFAISSAgentMemory:\n    \"\"\"Sanity tests for FAISS AgentMemory implementation.\"\"\"\n\n    def test_faiss_import(self):\n        \"\"\"Test that FAISSAgentMemory can be imported.\"\"\"\n        try:\n            from vanna.integrations.faiss import FAISSAgentMemory\n\n            assert FAISSAgentMemory is not None\n        except ImportError:\n            pytest.skip(\"FAISS not installed\")\n\n    def test_faiss_implements_agent_memory(self):\n        \"\"\"Test that FAISSAgentMemory implements AgentMemory.\"\"\"\n        try:\n            from vanna.integrations.faiss import FAISSAgentMemory\n            from vanna.capabilities.agent_memory import AgentMemory\n\n            assert issubclass(FAISSAgentMemory, AgentMemory)\n        except ImportError:\n            pytest.skip(\"FAISS not installed\")\n\n    def test_faiss_has_all_methods(self):\n        \"\"\"Test that FAISSAgentMemory implements all required methods.\"\"\"\n        try:\n            from vanna.integrations.faiss import FAISSAgentMemory\n\n            required_methods = [\n                \"save_tool_usage\",\n                \"save_text_memory\",\n                \"search_similar_usage\",\n                \"search_text_memories\",\n                \"get_recent_memories\",\n                \"get_recent_text_memories\",\n                \"delete_by_id\",\n                \"delete_text_memory\",\n                \"clear_memories\",\n            ]\n\n            for method_name in required_methods:\n                assert hasattr(FAISSAgentMemory, method_name)\n        except ImportError:\n            pytest.skip(\"FAISS not installed\")\n\n    def test_faiss_instantiation(self):\n        \"\"\"Test that FAISSAgentMemory can be instantiated.\"\"\"\n        try:\n            from vanna.integrations.faiss import FAISSAgentMemory\n            import tempfile\n\n            temp_dir = tempfile.mkdtemp()\n            memory = FAISSAgentMemory(persist_path=temp_dir)\n\n            assert memory is not None\n            assert memory.persist_path == temp_dir\n        except ImportError:\n            pytest.skip(\"FAISS not installed\")\n\n\nclass TestOpenSearchAgentMemory:\n    \"\"\"Sanity tests for OpenSearch AgentMemory implementation.\"\"\"\n\n    def test_opensearch_import(self):\n        \"\"\"Test that OpenSearchAgentMemory can be imported.\"\"\"\n        try:\n            from vanna.integrations.opensearch import OpenSearchAgentMemory\n\n            assert OpenSearchAgentMemory is not None\n        except ImportError:\n            pytest.skip(\"OpenSearch not installed\")\n\n    def test_opensearch_implements_agent_memory(self):\n        \"\"\"Test that OpenSearchAgentMemory implements AgentMemory.\"\"\"\n        try:\n            from vanna.integrations.opensearch import OpenSearchAgentMemory\n            from vanna.capabilities.agent_memory import AgentMemory\n\n            assert issubclass(OpenSearchAgentMemory, AgentMemory)\n        except ImportError:\n            pytest.skip(\"OpenSearch not installed\")\n\n    def test_opensearch_has_all_methods(self):\n        \"\"\"Test that OpenSearchAgentMemory implements all required methods.\"\"\"\n        try:\n            from vanna.integrations.opensearch import OpenSearchAgentMemory\n\n            required_methods = [\n                \"save_tool_usage\",\n                \"save_text_memory\",\n                \"search_similar_usage\",\n                \"search_text_memories\",\n                \"get_recent_memories\",\n                \"get_recent_text_memories\",\n                \"delete_by_id\",\n                \"delete_text_memory\",\n                \"clear_memories\",\n            ]\n\n            for method_name in required_methods:\n                assert hasattr(OpenSearchAgentMemory, method_name)\n        except ImportError:\n            pytest.skip(\"OpenSearch not installed\")\n\n\nclass TestAzureAISearchAgentMemory:\n    \"\"\"Sanity tests for Azure AI Search AgentMemory implementation.\"\"\"\n\n    def test_azuresearch_import(self):\n        \"\"\"Test that AzureAISearchAgentMemory can be imported.\"\"\"\n        try:\n            from vanna.integrations.azuresearch import AzureAISearchAgentMemory\n\n            assert AzureAISearchAgentMemory is not None\n        except ImportError:\n            pytest.skip(\"Azure Search not installed\")\n\n    def test_azuresearch_implements_agent_memory(self):\n        \"\"\"Test that AzureAISearchAgentMemory implements AgentMemory.\"\"\"\n        try:\n            from vanna.integrations.azuresearch import AzureAISearchAgentMemory\n            from vanna.capabilities.agent_memory import AgentMemory\n\n            assert issubclass(AzureAISearchAgentMemory, AgentMemory)\n        except ImportError:\n            pytest.skip(\"Azure Search not installed\")\n\n    def test_azuresearch_has_all_methods(self):\n        \"\"\"Test that AzureAISearchAgentMemory implements all required methods.\"\"\"\n        try:\n            from vanna.integrations.azuresearch import AzureAISearchAgentMemory\n\n            required_methods = [\n                \"save_tool_usage\",\n                \"save_text_memory\",\n                \"search_similar_usage\",\n                \"search_text_memories\",\n                \"get_recent_memories\",\n                \"get_recent_text_memories\",\n                \"delete_by_id\",\n                \"delete_text_memory\",\n                \"clear_memories\",\n            ]\n\n            for method_name in required_methods:\n                assert hasattr(AzureAISearchAgentMemory, method_name)\n        except ImportError:\n            pytest.skip(\"Azure Search not installed\")\n\n\nclass TestMarqoAgentMemory:\n    \"\"\"Sanity tests for Marqo AgentMemory implementation.\"\"\"\n\n    def test_marqo_import(self):\n        \"\"\"Test that MarqoAgentMemory can be imported.\"\"\"\n        try:\n            from vanna.integrations.marqo import MarqoAgentMemory\n\n            assert MarqoAgentMemory is not None\n        except ImportError:\n            pytest.skip(\"Marqo not installed\")\n\n    def test_marqo_implements_agent_memory(self):\n        \"\"\"Test that MarqoAgentMemory implements AgentMemory.\"\"\"\n        try:\n            from vanna.integrations.marqo import MarqoAgentMemory\n            from vanna.capabilities.agent_memory import AgentMemory\n\n            assert issubclass(MarqoAgentMemory, AgentMemory)\n        except ImportError:\n            pytest.skip(\"Marqo not installed\")\n\n    def test_marqo_has_all_methods(self):\n        \"\"\"Test that MarqoAgentMemory implements all required methods.\"\"\"\n        try:\n            from vanna.integrations.marqo import MarqoAgentMemory\n\n            required_methods = [\n                \"save_tool_usage\",\n                \"save_text_memory\",\n                \"search_similar_usage\",\n                \"search_text_memories\",\n                \"get_recent_memories\",\n                \"get_recent_text_memories\",\n                \"delete_by_id\",\n                \"delete_text_memory\",\n                \"clear_memories\",\n            ]\n\n            for method_name in required_methods:\n                assert hasattr(MarqoAgentMemory, method_name)\n        except ImportError:\n            pytest.skip(\"Marqo not installed\")\n\n\nclass TestDemoAgentMemory:\n    \"\"\"Sanity tests for DemoAgentMemory (in-memory) implementation.\"\"\"\n\n    def test_demo_import(self):\n        \"\"\"Test that DemoAgentMemory can be imported.\"\"\"\n        from vanna.integrations.local.agent_memory import DemoAgentMemory\n\n        assert DemoAgentMemory is not None\n\n    def test_demo_implements_agent_memory(self):\n        \"\"\"Test that DemoAgentMemory implements AgentMemory.\"\"\"\n        from vanna.integrations.local.agent_memory import DemoAgentMemory\n        from vanna.capabilities.agent_memory import AgentMemory\n\n        assert issubclass(DemoAgentMemory, AgentMemory)\n\n    def test_demo_has_all_methods(self):\n        \"\"\"Test that DemoAgentMemory implements all required methods.\"\"\"\n        from vanna.integrations.local.agent_memory import DemoAgentMemory\n\n        required_methods = [\n            \"save_tool_usage\",\n            \"save_text_memory\",\n            \"search_similar_usage\",\n            \"search_text_memories\",\n            \"get_recent_memories\",\n            \"get_recent_text_memories\",\n            \"delete_by_id\",\n            \"delete_text_memory\",\n            \"clear_memories\",\n        ]\n\n        for method_name in required_methods:\n            assert hasattr(DemoAgentMemory, method_name)\n\n    def test_demo_instantiation(self):\n        \"\"\"Test that DemoAgentMemory can be instantiated.\"\"\"\n        from vanna.integrations.local.agent_memory import DemoAgentMemory\n\n        memory = DemoAgentMemory(max_items=100)\n\n        assert memory is not None\n        assert memory._max_items == 100\n"
  },
  {
    "path": "tests/test_agents.py",
    "content": "\"\"\"\nSimple end-to-end tests for Vanna agents.\n\nTests use agent.send_message and validate the response components.\n\"\"\"\n\nimport os\nimport pytest\nfrom vanna.core.user import User\nfrom vanna.core.user.resolver import UserResolver\nfrom vanna.core.user.request_context import RequestContext\n\n\nclass SimpleUserResolver(UserResolver):\n    \"\"\"Simple user resolver for tests - always returns the same test user.\"\"\"\n\n    async def resolve_user(self, request_context: RequestContext) -> User:\n        return User(\n            id=\"test_user\", email=\"test@example.com\", group_memberships=[\"user\"]\n        )\n\n\ndef create_agent(llm_service, sql_runner):\n    \"\"\"Helper to create a configured agent.\"\"\"\n    from vanna import Agent, AgentConfig\n    from vanna.core.registry import ToolRegistry\n    from vanna.tools import RunSqlTool\n    from vanna.integrations.local.file_system import LocalFileSystem\n    from vanna.integrations.local.agent_memory import DemoAgentMemory\n    from vanna.tools.agent_memory import (\n        SaveQuestionToolArgsTool,\n        SearchSavedCorrectToolUsesTool,\n    )\n\n    tools = ToolRegistry()\n\n    # Add SQL tool\n    db_tool = RunSqlTool(sql_runner=sql_runner, file_system=LocalFileSystem())\n    tools.register_local_tool(db_tool, access_groups=[\"user\"])\n\n    # Add memory tools (they access agent_memory via ToolContext)\n    agent_memory = DemoAgentMemory(max_items=1000)\n    tools.register_local_tool(SaveQuestionToolArgsTool(), access_groups=[\"user\"])\n    tools.register_local_tool(SearchSavedCorrectToolUsesTool(), access_groups=[\"user\"])\n\n    return Agent(\n        llm_service=llm_service,\n        tool_registry=tools,\n        user_resolver=SimpleUserResolver(),\n        agent_memory=agent_memory,\n        config=AgentConfig(),\n    )\n\n\nasync def test_agent_top_artist(agent, expected_artist=\"Iron Maiden\"):\n    \"\"\"Common test logic for testing agent responses about top artist by sales.\"\"\"\n    # Create a simple request context\n    request_context = RequestContext(cookies={}, headers={})\n\n    # Collect all components from the async generator\n    components = []\n    async for component in agent.send_message(\n        request_context, \"Who is the top artist by sales?\"\n    ):\n        components.append(component)\n\n    # Validate we got components\n    assert len(components) > 0, \"Should receive at least one component\"\n\n    # Print all components for debugging\n    print(f\"\\n\\n=== Received {len(components)} components ===\")\n    for i, component in enumerate(components):\n        print(f\"\\nComponent {i + 1}:\")\n        print(f\"  Type: {component.type if hasattr(component, 'type') else 'no type'}\")\n        if hasattr(component, \"text\"):\n            print(\n                f\"  Text: {component.text[:200]}...\"\n                if len(component.text) > 200\n                else f\"  Text: {component.text}\"\n            )\n        if hasattr(component, \"content\"):\n            print(f\"  Content: {str(component.content)[:200]}...\")\n        print(f\"  Full: {component}\")\n\n    # Look for the expected artist in any component\n    found_artist = False\n    for component in components:\n        # Check rich_component.content\n        if hasattr(component, \"rich_component\") and hasattr(\n            component.rich_component, \"content\"\n        ):\n            if expected_artist in component.rich_component.content:\n                found_artist = True\n                break\n        # Check simple_component.text\n        if hasattr(component, \"simple_component\") and hasattr(\n            component.simple_component, \"text\"\n        ):\n            if expected_artist in component.simple_component.text:\n                found_artist = True\n                break\n\n    assert found_artist, (\n        f\"Response should mention '{expected_artist}' as the top artist. Got {len(components)} components.\"\n    )\n\n\n@pytest.mark.anthropic\n@pytest.mark.asyncio\nasync def test_anthropic_top_artist(chinook_db):\n    \"\"\"Test Anthropic agent finding the top artist by sales.\"\"\"\n    from vanna.integrations.anthropic import AnthropicLlmService\n\n    api_key = os.getenv(\"ANTHROPIC_API_KEY\")\n    llm = AnthropicLlmService(api_key=api_key, model=\"claude-sonnet-4-5\")\n\n    agent = create_agent(llm, chinook_db)\n    await test_agent_top_artist(agent)\n\n\n@pytest.mark.openai\n@pytest.mark.asyncio\nasync def test_openai_top_artist(chinook_db):\n    \"\"\"Test OpenAI agent finding the top artist by sales.\"\"\"\n    from vanna.integrations.openai import OpenAILlmService\n\n    api_key = os.getenv(\"OPENAI_API_KEY\")\n    llm = OpenAILlmService(api_key=api_key, model=\"gpt-5\")\n\n    agent = create_agent(llm, chinook_db)\n    await test_agent_top_artist(agent)\n\n\n@pytest.mark.azureopenai\n@pytest.mark.asyncio\nasync def test_azure_openai_top_artist(chinook_db):\n    \"\"\"Test Azure OpenAI agent finding the top artist by sales.\"\"\"\n    from vanna.integrations.azureopenai import AzureOpenAILlmService\n\n    # Get Azure OpenAI credentials from environment\n    api_key = os.getenv(\"AZURE_OPENAI_API_KEY\")\n    model = os.getenv(\"AZURE_OPENAI_MODEL\")\n    azure_endpoint = os.getenv(\"AZURE_OPENAI_ENDPOINT\")\n    api_version = os.getenv(\"AZURE_OPENAI_API_VERSION\")\n\n    llm = AzureOpenAILlmService(\n        model=model,\n        api_key=api_key,\n        azure_endpoint=azure_endpoint,\n        api_version=api_version,\n    )\n\n    agent = create_agent(llm, chinook_db)\n    await test_agent_top_artist(agent)\n\n\n# @pytest.mark.openai\n# @pytest.mark.asyncio\n# async def test_openai_responses_top_artist(chinook_db):\n#     \"\"\"Test OpenAI Responses API agent finding the top artist by sales.\"\"\"\n#     from vanna.integrations.openai import OpenAIResponsesService\n\n#     api_key = os.getenv(\"OPENAI_API_KEY\")\n#     llm = OpenAIResponsesService(api_key=api_key, model=\"gpt-5\")\n\n#     agent = create_agent(llm, chinook_db)\n#     await test_agent_top_artist(agent)\n\n\n@pytest.mark.ollama\n@pytest.mark.asyncio\nasync def test_ollama_top_artist(chinook_db):\n    \"\"\"Test Ollama agent finding the top artist by sales.\"\"\"\n    from vanna.integrations.ollama import OllamaLlmService\n\n    llm = OllamaLlmService(\n        model=\"gpt-oss:20b-cloud\",\n        host=os.getenv(\"OLLAMA_HOST\", \"http://localhost:11434\"),\n    )\n\n    agent = create_agent(llm, chinook_db)\n    await test_agent_top_artist(agent)\n\n\n@pytest.mark.gemini\n@pytest.mark.asyncio\nasync def test_gemini_top_artist(chinook_db):\n    \"\"\"Test Gemini agent finding the top artist by sales.\"\"\"\n    from vanna.integrations.google import GeminiLlmService\n\n    # API key will be picked up from GOOGLE_API_KEY or GEMINI_API_KEY env var\n    llm = GeminiLlmService(model=\"gemini-2.5-pro\", temperature=0.0)\n\n    agent = create_agent(llm, chinook_db)\n    await test_agent_top_artist(agent)\n"
  },
  {
    "path": "tests/test_azureopenai_llm.py",
    "content": "\"\"\"\nUnit tests for Azure OpenAI LLM service integration.\n\nThese tests validate the Azure OpenAI integration without making actual API calls.\n\"\"\"\n\nimport pytest\nfrom unittest.mock import Mock, patch, AsyncMock\nfrom vanna.integrations.azureopenai import AzureOpenAILlmService\nfrom vanna.integrations.azureopenai.llm import _is_reasoning_model\n\n\nclass TestReasoningModelDetection:\n    \"\"\"Test reasoning model detection logic.\"\"\"\n\n    def test_is_reasoning_model_o1(self):\n        \"\"\"Test that o1 models are detected as reasoning models.\"\"\"\n        assert _is_reasoning_model(\"o1\")\n        assert _is_reasoning_model(\"o1-mini\")\n        assert _is_reasoning_model(\"o1-preview\")\n\n    def test_is_reasoning_model_o3(self):\n        \"\"\"Test that o3 models are detected as reasoning models.\"\"\"\n        assert _is_reasoning_model(\"o3-mini\")\n\n    def test_is_reasoning_model_gpt5(self):\n        \"\"\"Test that GPT-5 series models are detected as reasoning models.\"\"\"\n        assert _is_reasoning_model(\"gpt-5\")\n        assert _is_reasoning_model(\"gpt-5-mini\")\n        assert _is_reasoning_model(\"gpt-5-nano\")\n        assert _is_reasoning_model(\"gpt-5-pro\")\n        assert _is_reasoning_model(\"gpt-5-codex\")\n\n    def test_is_not_reasoning_model(self):\n        \"\"\"Test that standard models are not detected as reasoning models.\"\"\"\n        assert not _is_reasoning_model(\"gpt-4\")\n        assert not _is_reasoning_model(\"gpt-4o\")\n        assert not _is_reasoning_model(\"gpt-4-turbo\")\n        assert not _is_reasoning_model(\"gpt-3.5-turbo\")\n\n    def test_case_insensitive_detection(self):\n        \"\"\"Test that model detection is case insensitive.\"\"\"\n        assert _is_reasoning_model(\"GPT-5\")\n        assert _is_reasoning_model(\"O1-MINI\")\n        assert not _is_reasoning_model(\"GPT-4O\")\n\n\nclass TestAzureOpenAILlmServiceInitialization:\n    \"\"\"Test Azure OpenAI service initialization.\"\"\"\n\n    @patch(\"vanna.integrations.azureopenai.llm.AzureOpenAI\")\n    def test_init_with_all_params(self, mock_azure_openai):\n        \"\"\"Test initialization with all parameters provided.\"\"\"\n        service = AzureOpenAILlmService(\n            model=\"gpt-4o\",\n            api_key=\"test-key\",\n            azure_endpoint=\"https://test.openai.azure.com\",\n            api_version=\"2024-10-21\",\n        )\n\n        assert service.model == \"gpt-4o\"\n        assert not service._is_reasoning_model\n\n        # Verify AzureOpenAI was called with correct params\n        mock_azure_openai.assert_called_once()\n        call_kwargs = mock_azure_openai.call_args[1]\n        assert call_kwargs[\"azure_endpoint\"] == \"https://test.openai.azure.com\"\n        assert call_kwargs[\"api_version\"] == \"2024-10-21\"\n        assert call_kwargs[\"api_key\"] == \"test-key\"\n\n    @patch(\"vanna.integrations.azureopenai.llm.AzureOpenAI\")\n    def test_init_with_reasoning_model(self, mock_azure_openai):\n        \"\"\"Test initialization with a reasoning model.\"\"\"\n        service = AzureOpenAILlmService(\n            model=\"gpt-5\",\n            api_key=\"test-key\",\n            azure_endpoint=\"https://test.openai.azure.com\",\n        )\n\n        assert service.model == \"gpt-5\"\n        assert service._is_reasoning_model\n\n    @patch.dict(\n        \"os.environ\",\n        {\n            \"AZURE_OPENAI_MODEL\": \"gpt-4o-deployment\",\n            \"AZURE_OPENAI_API_KEY\": \"env-key\",\n            \"AZURE_OPENAI_ENDPOINT\": \"https://env.openai.azure.com\",\n            \"AZURE_OPENAI_API_VERSION\": \"2024-06-01\",\n        },\n    )\n    @patch(\"vanna.integrations.azureopenai.llm.AzureOpenAI\")\n    def test_init_from_environment(self, mock_azure_openai):\n        \"\"\"Test initialization from environment variables.\"\"\"\n        service = AzureOpenAILlmService()\n\n        assert service.model == \"gpt-4o-deployment\"\n\n        # Verify AzureOpenAI was called with env values\n        call_kwargs = mock_azure_openai.call_args[1]\n        assert call_kwargs[\"azure_endpoint\"] == \"https://env.openai.azure.com\"\n        assert call_kwargs[\"api_version\"] == \"2024-06-01\"\n        assert call_kwargs[\"api_key\"] == \"env-key\"\n\n    @patch(\"vanna.integrations.azureopenai.llm.AzureOpenAI\")\n    def test_init_missing_model_raises(self, mock_azure_openai):\n        \"\"\"Test that missing model parameter raises ValueError.\"\"\"\n        with pytest.raises(ValueError, match=\"model parameter.*is required\"):\n            AzureOpenAILlmService(\n                api_key=\"test-key\",\n                azure_endpoint=\"https://test.openai.azure.com\",\n            )\n\n    @patch(\"vanna.integrations.azureopenai.llm.AzureOpenAI\")\n    def test_init_missing_endpoint_raises(self, mock_azure_openai):\n        \"\"\"Test that missing azure_endpoint raises ValueError.\"\"\"\n        with pytest.raises(ValueError, match=\"azure_endpoint is required\"):\n            AzureOpenAILlmService(\n                model=\"gpt-4o\",\n                api_key=\"test-key\",\n            )\n\n    @patch(\"vanna.integrations.azureopenai.llm.AzureOpenAI\")\n    def test_init_missing_auth_raises(self, mock_azure_openai):\n        \"\"\"Test that missing authentication raises ValueError.\"\"\"\n        with pytest.raises(ValueError, match=\"Authentication required\"):\n            AzureOpenAILlmService(\n                model=\"gpt-4o\",\n                azure_endpoint=\"https://test.openai.azure.com\",\n            )\n\n    @patch(\"vanna.integrations.azureopenai.llm.AzureOpenAI\")\n    def test_init_with_azure_ad_token_provider(self, mock_azure_openai):\n        \"\"\"Test initialization with Azure AD token provider.\"\"\"\n        mock_token_provider = Mock()\n        service = AzureOpenAILlmService(\n            model=\"gpt-4o\",\n            azure_endpoint=\"https://test.openai.azure.com\",\n            azure_ad_token_provider=mock_token_provider,\n        )\n\n        # Verify token provider was used instead of API key\n        call_kwargs = mock_azure_openai.call_args[1]\n        assert call_kwargs[\"azure_ad_token_provider\"] == mock_token_provider\n        assert \"api_key\" not in call_kwargs\n\n    @patch(\"vanna.integrations.azureopenai.llm.AzureOpenAI\")\n    def test_init_default_api_version(self, mock_azure_openai):\n        \"\"\"Test that default API version is used when not specified.\"\"\"\n        service = AzureOpenAILlmService(\n            model=\"gpt-4o\",\n            api_key=\"test-key\",\n            azure_endpoint=\"https://test.openai.azure.com\",\n        )\n\n        # Verify default API version (2024-10-21)\n        call_kwargs = mock_azure_openai.call_args[1]\n        assert call_kwargs[\"api_version\"] == \"2024-10-21\"\n\n\nclass TestAzureOpenAILlmServicePayloadBuilding:\n    \"\"\"Test payload building for API requests.\"\"\"\n\n    @patch(\"vanna.integrations.azureopenai.llm.AzureOpenAI\")\n    def test_build_payload_includes_temperature_for_standard_model(\n        self, mock_azure_openai\n    ):\n        \"\"\"Test that temperature is included for standard models.\"\"\"\n        from vanna.core.llm import LlmRequest, LlmMessage\n        from vanna.core.user import User\n\n        service = AzureOpenAILlmService(\n            model=\"gpt-4o\",\n            api_key=\"test-key\",\n            azure_endpoint=\"https://test.openai.azure.com\",\n        )\n\n        request = LlmRequest(\n            messages=[LlmMessage(role=\"user\", content=\"test\")],\n            user=User(id=\"test\", email=\"test@example.com\", group_memberships=[]),\n            temperature=0.8,\n        )\n\n        payload = service._build_payload(request)\n\n        assert \"temperature\" in payload\n        assert payload[\"temperature\"] == 0.8\n        assert payload[\"model\"] == \"gpt-4o\"\n\n    @patch(\"vanna.integrations.azureopenai.llm.AzureOpenAI\")\n    def test_build_payload_excludes_temperature_for_reasoning_model(\n        self, mock_azure_openai\n    ):\n        \"\"\"Test that temperature is excluded for reasoning models.\"\"\"\n        from vanna.core.llm import LlmRequest, LlmMessage\n        from vanna.core.user import User\n\n        service = AzureOpenAILlmService(\n            model=\"gpt-5\",\n            api_key=\"test-key\",\n            azure_endpoint=\"https://test.openai.azure.com\",\n        )\n\n        request = LlmRequest(\n            messages=[LlmMessage(role=\"user\", content=\"test\")],\n            user=User(id=\"test\", email=\"test@example.com\", group_memberships=[]),\n            temperature=0.8,\n        )\n\n        payload = service._build_payload(request)\n\n        assert \"temperature\" not in payload\n        assert payload[\"model\"] == \"gpt-5\"\n\n    @patch(\"vanna.integrations.azureopenai.llm.AzureOpenAI\")\n    def test_build_payload_with_system_prompt(self, mock_azure_openai):\n        \"\"\"Test that system prompt is added to messages.\"\"\"\n        from vanna.core.llm import LlmRequest, LlmMessage\n        from vanna.core.user import User\n\n        service = AzureOpenAILlmService(\n            model=\"gpt-4o\",\n            api_key=\"test-key\",\n            azure_endpoint=\"https://test.openai.azure.com\",\n        )\n\n        request = LlmRequest(\n            messages=[LlmMessage(role=\"user\", content=\"test\")],\n            user=User(id=\"test\", email=\"test@example.com\", group_memberships=[]),\n            system_prompt=\"You are a helpful assistant.\",\n        )\n\n        payload = service._build_payload(request)\n\n        assert len(payload[\"messages\"]) == 2\n        assert payload[\"messages\"][0][\"role\"] == \"system\"\n        assert payload[\"messages\"][0][\"content\"] == \"You are a helpful assistant.\"\n\n    @patch(\"vanna.integrations.azureopenai.llm.AzureOpenAI\")\n    def test_build_payload_with_tools(self, mock_azure_openai):\n        \"\"\"Test that tools are properly formatted in payload.\"\"\"\n        from vanna.core.llm import LlmRequest, LlmMessage\n        from vanna.core.user import User\n        from vanna.core.tool import ToolSchema\n\n        service = AzureOpenAILlmService(\n            model=\"gpt-4o\",\n            api_key=\"test-key\",\n            azure_endpoint=\"https://test.openai.azure.com\",\n        )\n\n        tool = ToolSchema(\n            name=\"test_tool\",\n            description=\"A test tool\",\n            parameters={\n                \"type\": \"object\",\n                \"properties\": {\"param1\": {\"type\": \"string\"}},\n            },\n        )\n\n        request = LlmRequest(\n            messages=[LlmMessage(role=\"user\", content=\"test\")],\n            user=User(id=\"test\", email=\"test@example.com\", group_memberships=[]),\n            tools=[tool],\n        )\n\n        payload = service._build_payload(request)\n\n        assert \"tools\" in payload\n        assert len(payload[\"tools\"]) == 1\n        assert payload[\"tools\"][0][\"type\"] == \"function\"\n        assert payload[\"tools\"][0][\"function\"][\"name\"] == \"test_tool\"\n        assert payload[\"tool_choice\"] == \"auto\"\n\n\nclass TestImportError:\n    \"\"\"Test import error handling.\"\"\"\n\n    def test_import_error_message(self):\n        \"\"\"Test that helpful error message is shown when openai is not installed.\"\"\"\n        with patch.dict(\"sys.modules\", {\"openai\": None}):\n            # Force module reload to trigger import error\n            import sys\n\n            if \"vanna.integrations.azureopenai.llm\" in sys.modules:\n                del sys.modules[\"vanna.integrations.azureopenai.llm\"]\n\n            with pytest.raises(\n                ImportError, match=\"pip install 'vanna\\\\[azureopenai\\\\]'\"\n            ):\n                from vanna.integrations.azureopenai import AzureOpenAILlmService\n\n                AzureOpenAILlmService(\n                    model=\"gpt-4o\",\n                    api_key=\"test\",\n                    azure_endpoint=\"https://test.openai.azure.com\",\n                )\n"
  },
  {
    "path": "tests/test_chromadb_persistence_fix.py",
    "content": "\"\"\"\nTest for ChromaDB persistence fix.\n\nThis test verifies that ChromaDB collections can be retrieved without triggering\nunnecessary embedding function initialization/model downloads.\n\"\"\"\n\nimport pytest\nimport tempfile\nimport shutil\nimport asyncio\n\nfrom vanna.integrations.chromadb import ChromaAgentMemory\nfrom vanna.core.user import User\nfrom vanna.core.tool import ToolContext\n\n\n@pytest.fixture\ndef test_user():\n    \"\"\"Test user for context.\"\"\"\n    return User(\n        id=\"test_user\",\n        username=\"test\",\n        email=\"test@example.com\",\n        group_memberships=[\"user\"],\n    )\n\n\ndef create_test_context(test_user, agent_memory):\n    \"\"\"Helper to create test context.\"\"\"\n    return ToolContext(\n        user=test_user,\n        conversation_id=\"test_conv\",\n        request_id=\"test_req\",\n        agent_memory=agent_memory,\n        metadata={},\n    )\n\n\n@pytest.mark.asyncio\nasync def test_chromadb_collection_retrieval_without_embedding_function(test_user):\n    \"\"\"\n    Test that existing ChromaDB collections can be retrieved without\n    initializing the embedding function (avoiding model downloads).\n\n    This test simulates the real-world scenario where:\n    1. A collection is created with an embedding function (first app run)\n    2. The app restarts and retrieves the existing collection\n    3. The embedding function should NOT be re-initialized on retrieval\n    \"\"\"\n    try:\n        import chromadb\n        from chromadb.config import Settings\n    except ImportError:\n        pytest.skip(\"ChromaDB not installed\")\n\n    temp_dir = tempfile.mkdtemp()\n\n    try:\n        # Session 1: Create a collection using ChromaAgentMemory (simulating first app run)\n        # This will create the collection with an embedding function\n        memory1 = ChromaAgentMemory(\n            persist_directory=temp_dir, collection_name=\"test_collection\"\n        )\n\n        context = create_test_context(test_user, memory1)\n\n        # Save some memories (this will create the collection)\n        # We need to add explicit embeddings to avoid model download in test environment\n        collection = memory1._get_collection()\n        collection.add(\n            ids=[\"mem1\", \"mem2\"],\n            documents=[\"test question 1\", \"test question 2\"],\n            embeddings=[[0.1] * 384, [0.2] * 384],\n            metadatas=[\n                {\n                    \"question\": \"test question 1\",\n                    \"tool_name\": \"test_tool\",\n                    \"args_json\": \"{}\",\n                    \"timestamp\": \"2024-01-01T00:00:00\",\n                    \"success\": True,\n                    \"metadata_json\": \"{}\",\n                },\n                {\n                    \"question\": \"test question 2\",\n                    \"tool_name\": \"test_tool\",\n                    \"args_json\": \"{}\",\n                    \"timestamp\": \"2024-01-01T00:01:00\",\n                    \"success\": True,\n                    \"metadata_json\": \"{}\",\n                },\n            ],\n        )\n\n        # Clean up references to simulate app restart\n        del collection\n        del memory1\n\n        # Session 2: Create new ChromaAgentMemory instance (simulating app restart)\n        # This should retrieve the existing collection WITHOUT calling _get_embedding_function\n        memory2 = ChromaAgentMemory(\n            persist_directory=temp_dir, collection_name=\"test_collection\"\n        )\n\n        # Mock _get_embedding_function to verify it's not called\n        original_get_ef = memory2._get_embedding_function\n\n        def mock_get_ef():\n            pytest.fail(\n                \"_get_embedding_function was called when retrieving existing collection\"\n            )\n\n        memory2._get_embedding_function = mock_get_ef\n\n        # This should retrieve the existing collection without calling _get_embedding_function\n        collection2 = memory2._get_collection()\n\n        # Restore original method\n        memory2._get_embedding_function = original_get_ef\n\n        # Verify collection was retrieved successfully\n        assert collection2 is not None\n        assert collection2.name == \"test_collection\"\n        assert collection2.count() == 2\n\n        # Test that we can use public API methods on the retrieved collection\n        context2 = create_test_context(test_user, memory2)\n        recent = await memory2.get_recent_memories(context=context2, limit=10)\n        assert len(recent) == 2\n        assert recent[0].question in [\"test question 1\", \"test question 2\"]\n\n    finally:\n        shutil.rmtree(temp_dir, ignore_errors=True)\n\n\n@pytest.mark.asyncio\nasync def test_chromadb_collection_creation_with_embedding_function():\n    \"\"\"\n    Test that NEW ChromaDB collections are created WITH the embedding function.\n    \"\"\"\n    try:\n        from vanna.integrations.chromadb import ChromaAgentMemory\n    except ImportError:\n        pytest.skip(\"ChromaDB not installed\")\n\n    temp_dir = tempfile.mkdtemp()\n\n    try:\n        # Test: Create ChromaAgentMemory for a non-existent collection\n        memory = ChromaAgentMemory(\n            persist_directory=temp_dir, collection_name=\"new_collection\"\n        )\n\n        # Track if _get_embedding_function was called\n        get_ef_called = []\n        original_get_ef = memory._get_embedding_function\n\n        def tracking_get_ef():\n            get_ef_called.append(True)\n            return original_get_ef()\n\n        memory._get_embedding_function = tracking_get_ef\n\n        # This should create a new collection and SHOULD call _get_embedding_function\n        collection = memory._get_collection()\n\n        # Restore original\n        memory._get_embedding_function = original_get_ef\n\n        # Verify collection was created\n        assert collection is not None\n        assert collection.name == \"new_collection\"\n\n        # Verify _get_embedding_function was called\n        assert get_ef_called, (\n            \"_get_embedding_function should be called when creating new collection\"\n        )\n\n    finally:\n        shutil.rmtree(temp_dir, ignore_errors=True)\n\n\nif __name__ == \"__main__\":\n    # Run tests directly\n    asyncio.run(test_chromadb_collection_retrieval_without_embedding_function())\n    asyncio.run(test_chromadb_collection_creation_with_embedding_function())\n"
  },
  {
    "path": "tests/test_database_sanity.py",
    "content": "\"\"\"\nSanity tests for database implementations.\n\nThese tests verify that:\n1. Each database implementation correctly implements the SqlRunner interface\n2. Imports are working correctly for all database modules\n3. Basic class instantiation works (without requiring actual database connections)\n\nNote: These tests do NOT execute actual queries against databases.\nThey are lightweight sanity checks for the implementation structure.\n\"\"\"\n\nimport pytest\nfrom abc import abstractmethod\nfrom inspect import signature, iscoroutinefunction\nimport pandas as pd\n\n\nclass TestSqlRunnerInterface:\n    \"\"\"Test that the SqlRunner interface is properly defined.\"\"\"\n\n    def test_sql_runner_import(self):\n        \"\"\"Test that SqlRunner can be imported.\"\"\"\n        from vanna.capabilities.sql_runner import SqlRunner\n\n        assert SqlRunner is not None\n\n    def test_sql_runner_is_abstract(self):\n        \"\"\"Test that SqlRunner is an abstract base class.\"\"\"\n        from vanna.capabilities.sql_runner import SqlRunner\n        from abc import ABC\n\n        assert issubclass(SqlRunner, ABC)\n\n    def test_sql_runner_has_run_sql_method(self):\n        \"\"\"Test that SqlRunner defines the run_sql abstract method.\"\"\"\n        from vanna.capabilities.sql_runner import SqlRunner\n\n        assert hasattr(SqlRunner, \"run_sql\")\n        assert getattr(SqlRunner.run_sql, \"__isabstractmethod__\", False)\n\n    def test_run_sql_method_signature(self):\n        \"\"\"Test that run_sql has the correct method signature.\"\"\"\n        from vanna.capabilities.sql_runner import SqlRunner\n\n        sig = signature(SqlRunner.run_sql)\n        params = list(sig.parameters.keys())\n\n        # Should have: self, args, context\n        assert len(params) == 3\n        assert params[0] == \"self\"\n        assert params[1] == \"args\"\n        assert params[2] == \"context\"\n\n    def test_run_sql_is_async(self):\n        \"\"\"Test that run_sql is defined as an async method.\"\"\"\n        from vanna.capabilities.sql_runner import SqlRunner\n\n        # Abstract methods are wrapped, so we check if it's meant to be async\n        # by looking at the method definition\n        assert iscoroutinefunction(SqlRunner.run_sql)\n\n\nclass TestRunSqlToolArgsModel:\n    \"\"\"Test the RunSqlToolArgs model.\"\"\"\n\n    def test_run_sql_tool_args_import(self):\n        \"\"\"Test that RunSqlToolArgs can be imported.\"\"\"\n        from vanna.capabilities.sql_runner import RunSqlToolArgs\n\n        assert RunSqlToolArgs is not None\n\n    def test_run_sql_tool_args_has_sql_field(self):\n        \"\"\"Test that RunSqlToolArgs has a 'sql' field.\"\"\"\n        from vanna.capabilities.sql_runner import RunSqlToolArgs\n\n        # Create an instance\n        args = RunSqlToolArgs(sql=\"SELECT 1\")\n        assert hasattr(args, \"sql\")\n        assert args.sql == \"SELECT 1\"\n\n    def test_run_sql_tool_args_is_pydantic_model(self):\n        \"\"\"Test that RunSqlToolArgs is a Pydantic model.\"\"\"\n        from vanna.capabilities.sql_runner import RunSqlToolArgs\n        from pydantic import BaseModel\n\n        assert issubclass(RunSqlToolArgs, BaseModel)\n\n\nclass TestPostgresRunner:\n    \"\"\"Sanity tests for PostgresRunner implementation.\"\"\"\n\n    def test_postgres_runner_import(self):\n        \"\"\"Test that PostgresRunner can be imported.\"\"\"\n        from vanna.integrations.postgres import PostgresRunner\n\n        assert PostgresRunner is not None\n\n    def test_postgres_runner_implements_sql_runner(self):\n        \"\"\"Test that PostgresRunner implements SqlRunner interface.\"\"\"\n        from vanna.integrations.postgres import PostgresRunner\n        from vanna.capabilities.sql_runner import SqlRunner\n\n        assert issubclass(PostgresRunner, SqlRunner)\n\n    def test_postgres_runner_has_run_sql_method(self):\n        \"\"\"Test that PostgresRunner implements run_sql method.\"\"\"\n        from vanna.integrations.postgres import PostgresRunner\n\n        assert hasattr(PostgresRunner, \"run_sql\")\n        # Should not be abstract anymore\n        assert not getattr(PostgresRunner.run_sql, \"__isabstractmethod__\", False)\n\n    def test_postgres_runner_instantiation_with_connection_string(self):\n        \"\"\"Test that PostgresRunner can be instantiated with connection string.\"\"\"\n        from vanna.integrations.postgres import PostgresRunner\n\n        # This should not raise an error (no actual connection is made in __init__)\n        runner = PostgresRunner(connection_string=\"postgresql://user:pass@localhost/db\")\n        assert runner is not None\n        assert runner.connection_string == \"postgresql://user:pass@localhost/db\"\n        assert runner.connection_params is None\n\n    def test_postgres_runner_instantiation_with_params(self):\n        \"\"\"Test that PostgresRunner can be instantiated with individual parameters.\"\"\"\n        from vanna.integrations.postgres import PostgresRunner\n\n        runner = PostgresRunner(\n            host=\"localhost\",\n            port=5432,\n            database=\"testdb\",\n            user=\"testuser\",\n            password=\"testpass\",\n        )\n        assert runner is not None\n        assert runner.connection_string is None\n        assert runner.connection_params is not None\n        assert runner.connection_params[\"host\"] == \"localhost\"\n        assert runner.connection_params[\"database\"] == \"testdb\"\n\n    def test_postgres_runner_requires_valid_params(self):\n        \"\"\"Test that PostgresRunner raises error with invalid parameters.\"\"\"\n        from vanna.integrations.postgres import PostgresRunner\n\n        with pytest.raises(ValueError, match=\"Either provide connection_string OR\"):\n            PostgresRunner()  # No parameters provided\n\n    def test_postgres_runner_checks_psycopg2_import(self):\n        \"\"\"Test that PostgresRunner checks for psycopg2 package.\"\"\"\n        from vanna.integrations.postgres import PostgresRunner\n\n        # If psycopg2 is not installed, this should raise ImportError\n        # If it is installed, this should work fine\n        try:\n            runner = PostgresRunner(connection_string=\"postgresql://test\")\n            assert runner.psycopg2 is not None\n        except ImportError as e:\n            assert \"psycopg2\" in str(e)\n\n\nclass TestSqliteRunner:\n    \"\"\"Sanity tests for SqliteRunner implementation.\"\"\"\n\n    def test_sqlite_runner_import(self):\n        \"\"\"Test that SqliteRunner can be imported.\"\"\"\n        from vanna.integrations.sqlite import SqliteRunner\n\n        assert SqliteRunner is not None\n\n    def test_sqlite_runner_implements_sql_runner(self):\n        \"\"\"Test that SqliteRunner implements SqlRunner interface.\"\"\"\n        from vanna.integrations.sqlite import SqliteRunner\n        from vanna.capabilities.sql_runner import SqlRunner\n\n        assert issubclass(SqliteRunner, SqlRunner)\n\n    def test_sqlite_runner_has_run_sql_method(self):\n        \"\"\"Test that SqliteRunner implements run_sql method.\"\"\"\n        from vanna.integrations.sqlite import SqliteRunner\n\n        assert hasattr(SqliteRunner, \"run_sql\")\n        # Should not be abstract anymore\n        assert not getattr(SqliteRunner.run_sql, \"__isabstractmethod__\", False)\n\n    def test_sqlite_runner_instantiation(self):\n        \"\"\"Test that SqliteRunner can be instantiated with a database path.\"\"\"\n        from vanna.integrations.sqlite import SqliteRunner\n\n        runner = SqliteRunner(database_path=\"/tmp/test.db\")\n        assert runner is not None\n        assert runner.database_path == \"/tmp/test.db\"\n\n    def test_sqlite_uses_builtin_sqlite3(self):\n        \"\"\"Test that SqliteRunner uses Python's built-in sqlite3 module.\"\"\"\n        import sqlite3\n        from vanna.integrations.sqlite import SqliteRunner\n\n        # sqlite3 should be importable (it's part of Python standard library)\n        assert sqlite3 is not None\n\n\nclass TestLegacySqlRunner:\n    \"\"\"Sanity tests for LegacySqlRunner adapter.\"\"\"\n\n    def test_legacy_sql_runner_import(self):\n        \"\"\"Test that LegacySqlRunner can be imported.\"\"\"\n        from vanna.legacy.adapter import LegacySqlRunner\n\n        assert LegacySqlRunner is not None\n\n    def test_legacy_sql_runner_implements_sql_runner(self):\n        \"\"\"Test that LegacySqlRunner implements SqlRunner interface.\"\"\"\n        from vanna.legacy.adapter import LegacySqlRunner\n        from vanna.capabilities.sql_runner import SqlRunner\n\n        assert issubclass(LegacySqlRunner, SqlRunner)\n\n    def test_legacy_sql_runner_has_run_sql_method(self):\n        \"\"\"Test that LegacySqlRunner implements run_sql method.\"\"\"\n        from vanna.legacy.adapter import LegacySqlRunner\n\n        assert hasattr(LegacySqlRunner, \"run_sql\")\n        # Should not be abstract anymore\n        assert not getattr(LegacySqlRunner.run_sql, \"__isabstractmethod__\", False)\n\n    def test_legacy_sql_runner_instantiation(self):\n        \"\"\"Test that LegacySqlRunner can be instantiated with a VannaBase instance.\"\"\"\n        from vanna.legacy.adapter import LegacySqlRunner\n        from unittest.mock import Mock\n\n        # Create a mock VannaBase instance\n        mock_vn = Mock()\n\n        runner = LegacySqlRunner(vn=mock_vn)\n        assert runner is not None\n        assert runner.vn is mock_vn\n\n\nclass TestDatabaseIntegrationModules:\n    \"\"\"Test that database integration modules can be imported.\"\"\"\n\n    def test_postgres_module_import(self):\n        \"\"\"Test that the postgres integration module can be imported.\"\"\"\n        try:\n            import vanna.integrations.postgres\n\n            assert vanna.integrations.postgres is not None\n        except ImportError as e:\n            pytest.fail(f\"Failed to import postgres module: {e}\")\n\n    def test_sqlite_module_import(self):\n        \"\"\"Test that the sqlite integration module can be imported.\"\"\"\n        try:\n            import vanna.integrations.sqlite\n\n            assert vanna.integrations.sqlite is not None\n        except ImportError as e:\n            pytest.fail(f\"Failed to import sqlite module: {e}\")\n\n    def test_postgres_module_exports_runner(self):\n        \"\"\"Test that postgres module exports PostgresRunner.\"\"\"\n        from vanna.integrations.postgres import PostgresRunner\n\n        assert PostgresRunner is not None\n\n    def test_sqlite_module_exports_runner(self):\n        \"\"\"Test that sqlite module exports SqliteRunner.\"\"\"\n        from vanna.integrations.sqlite import SqliteRunner\n\n        assert SqliteRunner is not None\n\n\nclass TestLegacyVannaBaseConnections:\n    \"\"\"Test that legacy VannaBase connection methods exist.\"\"\"\n\n    def test_vanna_base_import(self):\n        \"\"\"Test that VannaBase can be imported.\"\"\"\n        from vanna.legacy.base.base import VannaBase\n\n        assert VannaBase is not None\n\n    def test_vanna_base_has_connection_methods(self):\n        \"\"\"Test that VannaBase has various database connection methods.\"\"\"\n        from vanna.legacy.base.base import VannaBase\n\n        connection_methods = [\n            \"connect_to_snowflake\",\n            \"connect_to_sqlite\",\n            \"connect_to_postgres\",\n            \"connect_to_mysql\",\n            \"connect_to_clickhouse\",\n            \"connect_to_oracle\",\n            \"connect_to_bigquery\",\n            \"connect_to_duckdb\",\n            \"connect_to_mssql\",\n            \"connect_to_presto\",\n            \"connect_to_hive\",\n        ]\n\n        for method_name in connection_methods:\n            assert hasattr(VannaBase, method_name), f\"VannaBase missing {method_name}\"\n\n    def test_vanna_base_has_run_sql_method(self):\n        \"\"\"Test that VannaBase has a run_sql method.\"\"\"\n        from vanna.legacy.base.base import VannaBase\n\n        assert hasattr(VannaBase, \"run_sql\")\n\n\nclass TestLegacyVannaAdapter:\n    \"\"\"Test the LegacyVannaAdapter.\"\"\"\n\n    def test_legacy_vanna_adapter_import(self):\n        \"\"\"Test that LegacyVannaAdapter can be imported.\"\"\"\n        from vanna.legacy.adapter import LegacyVannaAdapter\n\n        assert LegacyVannaAdapter is not None\n\n    def test_legacy_vanna_adapter_is_tool_registry(self):\n        \"\"\"Test that LegacyVannaAdapter extends ToolRegistry.\"\"\"\n        from vanna.legacy.adapter import LegacyVannaAdapter\n        from vanna.core.registry import ToolRegistry\n\n        assert issubclass(LegacyVannaAdapter, ToolRegistry)\n\n\nclass TestSnowflakeRunner:\n    \"\"\"Sanity tests for SnowflakeRunner implementation.\"\"\"\n\n    def test_snowflake_runner_import(self):\n        \"\"\"Test that SnowflakeRunner can be imported.\"\"\"\n        from vanna.integrations.snowflake import SnowflakeRunner\n\n        assert SnowflakeRunner is not None\n\n    def test_snowflake_runner_implements_sql_runner(self):\n        \"\"\"Test that SnowflakeRunner implements SqlRunner interface.\"\"\"\n        from vanna.integrations.snowflake import SnowflakeRunner\n        from vanna.capabilities.sql_runner import SqlRunner\n\n        assert issubclass(SnowflakeRunner, SqlRunner)\n\n    def test_snowflake_runner_has_run_sql_method(self):\n        \"\"\"Test that SnowflakeRunner implements run_sql method.\"\"\"\n        from vanna.integrations.snowflake import SnowflakeRunner\n\n        assert hasattr(SnowflakeRunner, \"run_sql\")\n        assert not getattr(SnowflakeRunner.run_sql, \"__isabstractmethod__\", False)\n\n    def test_snowflake_runner_instantiation(self):\n        \"\"\"Test that SnowflakeRunner can be instantiated with required parameters.\"\"\"\n        from vanna.integrations.snowflake import SnowflakeRunner\n\n        runner = SnowflakeRunner(\n            account=\"test-account\",\n            username=\"test-user\",\n            password=\"test-pass\",\n            database=\"test-db\",\n        )\n        assert runner is not None\n        assert runner.account == \"test-account\"\n\n    def test_snowflake_runner_key_pair_auth_with_path(self, tmp_path):\n        \"\"\"Test that SnowflakeRunner can be instantiated with RSA key-pair authentication using path.\"\"\"\n        from vanna.integrations.snowflake import SnowflakeRunner\n\n        # Create a temporary private key file\n        private_key_file = tmp_path / \"test_private_key.p8\"\n        private_key_file.write_text(\n            \"-----BEGIN PRIVATE KEY-----\\ntest_key_content\\n-----END PRIVATE KEY-----\"\n        )\n\n        runner = SnowflakeRunner(\n            account=\"test-account\",\n            username=\"test-user\",\n            private_key_path=str(private_key_file),\n            private_key_passphrase=\"test-passphrase\",\n            database=\"test-db\",\n        )\n        assert runner is not None\n        assert runner.account == \"test-account\"\n        assert runner.username == \"test-user\"\n        assert runner.password is None\n        assert runner.private_key_path == str(private_key_file)\n        assert runner.private_key_passphrase == \"test-passphrase\"\n\n    def test_snowflake_runner_key_pair_auth_with_content(self):\n        \"\"\"Test that SnowflakeRunner can be instantiated with RSA key-pair authentication using content.\"\"\"\n        from vanna.integrations.snowflake import SnowflakeRunner\n\n        private_key_content = (\n            b\"-----BEGIN PRIVATE KEY-----\\ntest_key_content\\n-----END PRIVATE KEY-----\"\n        )\n\n        runner = SnowflakeRunner(\n            account=\"test-account\",\n            username=\"test-user\",\n            private_key_content=private_key_content,\n            database=\"test-db\",\n        )\n        assert runner is not None\n        assert runner.account == \"test-account\"\n        assert runner.username == \"test-user\"\n        assert runner.password is None\n        assert runner.private_key_content == private_key_content\n\n    def test_snowflake_runner_key_pair_auth_without_passphrase(self, tmp_path):\n        \"\"\"Test that SnowflakeRunner works with unencrypted private key (no passphrase).\"\"\"\n        from vanna.integrations.snowflake import SnowflakeRunner\n\n        # Create a temporary private key file\n        private_key_file = tmp_path / \"test_private_key_unencrypted.p8\"\n        private_key_file.write_text(\n            \"-----BEGIN PRIVATE KEY-----\\ntest_key_content\\n-----END PRIVATE KEY-----\"\n        )\n\n        runner = SnowflakeRunner(\n            account=\"test-account\",\n            username=\"test-user\",\n            private_key_path=str(private_key_file),\n            database=\"test-db\",\n        )\n        assert runner is not None\n        assert runner.private_key_passphrase is None\n\n    def test_snowflake_runner_missing_auth_raises_error(self):\n        \"\"\"Test that SnowflakeRunner raises error when no authentication method is provided.\"\"\"\n        from vanna.integrations.snowflake import SnowflakeRunner\n        import pytest\n\n        with pytest.raises(\n            ValueError,\n            match=\"Either password or private_key_path/private_key_content must be provided\",\n        ):\n            SnowflakeRunner(\n                account=\"test-account\", username=\"test-user\", database=\"test-db\"\n            )\n\n    def test_snowflake_runner_invalid_key_path_raises_error(self):\n        \"\"\"Test that SnowflakeRunner raises error when private key file doesn't exist.\"\"\"\n        from vanna.integrations.snowflake import SnowflakeRunner\n        import pytest\n\n        with pytest.raises(FileNotFoundError, match=\"Private key file not found\"):\n            SnowflakeRunner(\n                account=\"test-account\",\n                username=\"test-user\",\n                private_key_path=\"/nonexistent/path/to/key.p8\",\n                database=\"test-db\",\n            )\n\n    def test_snowflake_runner_password_auth_backwards_compatible(self):\n        \"\"\"Test that SnowflakeRunner maintains backward compatibility with password auth.\"\"\"\n        from vanna.integrations.snowflake import SnowflakeRunner\n\n        runner = SnowflakeRunner(\n            account=\"test-account\",\n            username=\"test-user\",\n            password=\"test-password\",\n            database=\"test-db\",\n            role=\"test-role\",\n            warehouse=\"test-warehouse\",\n        )\n        assert runner is not None\n        assert runner.password == \"test-password\"\n        assert runner.private_key_path is None\n        assert runner.private_key_content is None\n\n\nclass TestMySQLRunner:\n    \"\"\"Sanity tests for MySQLRunner implementation.\"\"\"\n\n    def test_mysql_runner_import(self):\n        \"\"\"Test that MySQLRunner can be imported.\"\"\"\n        from vanna.integrations.mysql import MySQLRunner\n\n        assert MySQLRunner is not None\n\n    def test_mysql_runner_implements_sql_runner(self):\n        \"\"\"Test that MySQLRunner implements SqlRunner interface.\"\"\"\n        from vanna.integrations.mysql import MySQLRunner\n        from vanna.capabilities.sql_runner import SqlRunner\n\n        assert issubclass(MySQLRunner, SqlRunner)\n\n    def test_mysql_runner_has_run_sql_method(self):\n        \"\"\"Test that MySQLRunner implements run_sql method.\"\"\"\n        from vanna.integrations.mysql import MySQLRunner\n\n        assert hasattr(MySQLRunner, \"run_sql\")\n        assert not getattr(MySQLRunner.run_sql, \"__isabstractmethod__\", False)\n\n    def test_mysql_runner_instantiation(self):\n        \"\"\"Test that MySQLRunner can be instantiated with required parameters.\"\"\"\n        from vanna.integrations.mysql import MySQLRunner\n\n        runner = MySQLRunner(\n            host=\"localhost\", database=\"test-db\", user=\"test-user\", password=\"test-pass\"\n        )\n        assert runner is not None\n        assert runner.host == \"localhost\"\n\n\nclass TestClickHouseRunner:\n    \"\"\"Sanity tests for ClickHouseRunner implementation.\"\"\"\n\n    def test_clickhouse_runner_import(self):\n        \"\"\"Test that ClickHouseRunner can be imported.\"\"\"\n        from vanna.integrations.clickhouse import ClickHouseRunner\n\n        assert ClickHouseRunner is not None\n\n    def test_clickhouse_runner_implements_sql_runner(self):\n        \"\"\"Test that ClickHouseRunner implements SqlRunner interface.\"\"\"\n        from vanna.integrations.clickhouse import ClickHouseRunner\n        from vanna.capabilities.sql_runner import SqlRunner\n\n        assert issubclass(ClickHouseRunner, SqlRunner)\n\n    def test_clickhouse_runner_has_run_sql_method(self):\n        \"\"\"Test that ClickHouseRunner implements run_sql method.\"\"\"\n        from vanna.integrations.clickhouse import ClickHouseRunner\n\n        assert hasattr(ClickHouseRunner, \"run_sql\")\n        assert not getattr(ClickHouseRunner.run_sql, \"__isabstractmethod__\", False)\n\n    def test_clickhouse_runner_instantiation(self):\n        \"\"\"Test that ClickHouseRunner can be instantiated with required parameters.\"\"\"\n        from vanna.integrations.clickhouse import ClickHouseRunner\n\n        runner = ClickHouseRunner(\n            host=\"localhost\", database=\"test-db\", user=\"test-user\", password=\"test-pass\"\n        )\n        assert runner is not None\n        assert runner.host == \"localhost\"\n\n\nclass TestOracleRunner:\n    \"\"\"Sanity tests for OracleRunner implementation.\"\"\"\n\n    def test_oracle_runner_import(self):\n        \"\"\"Test that OracleRunner can be imported.\"\"\"\n        from vanna.integrations.oracle import OracleRunner\n\n        assert OracleRunner is not None\n\n    def test_oracle_runner_implements_sql_runner(self):\n        \"\"\"Test that OracleRunner implements SqlRunner interface.\"\"\"\n        from vanna.integrations.oracle import OracleRunner\n        from vanna.capabilities.sql_runner import SqlRunner\n\n        assert issubclass(OracleRunner, SqlRunner)\n\n    def test_oracle_runner_has_run_sql_method(self):\n        \"\"\"Test that OracleRunner implements run_sql method.\"\"\"\n        from vanna.integrations.oracle import OracleRunner\n\n        assert hasattr(OracleRunner, \"run_sql\")\n        assert not getattr(OracleRunner.run_sql, \"__isabstractmethod__\", False)\n\n    def test_oracle_runner_instantiation(self):\n        \"\"\"Test that OracleRunner can be instantiated with required parameters.\"\"\"\n        from vanna.integrations.oracle import OracleRunner\n\n        runner = OracleRunner(\n            user=\"test-user\", password=\"test-pass\", dsn=\"localhost:1521/ORCL\"\n        )\n        assert runner is not None\n        assert runner.user == \"test-user\"\n\n\nclass TestBigQueryRunner:\n    \"\"\"Sanity tests for BigQueryRunner implementation.\"\"\"\n\n    def test_bigquery_runner_import(self):\n        \"\"\"Test that BigQueryRunner can be imported.\"\"\"\n        from vanna.integrations.bigquery import BigQueryRunner\n\n        assert BigQueryRunner is not None\n\n    def test_bigquery_runner_implements_sql_runner(self):\n        \"\"\"Test that BigQueryRunner implements SqlRunner interface.\"\"\"\n        from vanna.integrations.bigquery import BigQueryRunner\n        from vanna.capabilities.sql_runner import SqlRunner\n\n        assert issubclass(BigQueryRunner, SqlRunner)\n\n    def test_bigquery_runner_has_run_sql_method(self):\n        \"\"\"Test that BigQueryRunner implements run_sql method.\"\"\"\n        from vanna.integrations.bigquery import BigQueryRunner\n\n        assert hasattr(BigQueryRunner, \"run_sql\")\n        assert not getattr(BigQueryRunner.run_sql, \"__isabstractmethod__\", False)\n\n    def test_bigquery_runner_instantiation(self):\n        \"\"\"Test that BigQueryRunner can be instantiated with required parameters.\"\"\"\n        from vanna.integrations.bigquery import BigQueryRunner\n\n        runner = BigQueryRunner(project_id=\"test-project\")\n        assert runner is not None\n        assert runner.project_id == \"test-project\"\n\n\nclass TestDuckDBRunner:\n    \"\"\"Sanity tests for DuckDBRunner implementation.\"\"\"\n\n    def test_duckdb_runner_import(self):\n        \"\"\"Test that DuckDBRunner can be imported.\"\"\"\n        from vanna.integrations.duckdb import DuckDBRunner\n\n        assert DuckDBRunner is not None\n\n    def test_duckdb_runner_implements_sql_runner(self):\n        \"\"\"Test that DuckDBRunner implements SqlRunner interface.\"\"\"\n        from vanna.integrations.duckdb import DuckDBRunner\n        from vanna.capabilities.sql_runner import SqlRunner\n\n        assert issubclass(DuckDBRunner, SqlRunner)\n\n    def test_duckdb_runner_has_run_sql_method(self):\n        \"\"\"Test that DuckDBRunner implements run_sql method.\"\"\"\n        from vanna.integrations.duckdb import DuckDBRunner\n\n        assert hasattr(DuckDBRunner, \"run_sql\")\n        assert not getattr(DuckDBRunner.run_sql, \"__isabstractmethod__\", False)\n\n    def test_duckdb_runner_instantiation_memory(self):\n        \"\"\"Test that DuckDBRunner can be instantiated for in-memory database.\"\"\"\n        from vanna.integrations.duckdb import DuckDBRunner\n\n        runner = DuckDBRunner(database_path=\":memory:\")\n        assert runner is not None\n        assert runner.database_path == \":memory:\"\n\n    def test_duckdb_runner_instantiation_file(self):\n        \"\"\"Test that DuckDBRunner can be instantiated with file path.\"\"\"\n        from vanna.integrations.duckdb import DuckDBRunner\n\n        runner = DuckDBRunner(database_path=\"/tmp/test.duckdb\")\n        assert runner is not None\n        assert runner.database_path == \"/tmp/test.duckdb\"\n\n\nclass TestMSSQLRunner:\n    \"\"\"Sanity tests for MSSQLRunner implementation.\"\"\"\n\n    def test_mssql_runner_import(self):\n        \"\"\"Test that MSSQLRunner can be imported.\"\"\"\n        from vanna.integrations.mssql import MSSQLRunner\n\n        assert MSSQLRunner is not None\n\n    def test_mssql_runner_implements_sql_runner(self):\n        \"\"\"Test that MSSQLRunner implements SqlRunner interface.\"\"\"\n        from vanna.integrations.mssql import MSSQLRunner\n        from vanna.capabilities.sql_runner import SqlRunner\n\n        assert issubclass(MSSQLRunner, SqlRunner)\n\n    def test_mssql_runner_has_run_sql_method(self):\n        \"\"\"Test that MSSQLRunner implements run_sql method.\"\"\"\n        from vanna.integrations.mssql import MSSQLRunner\n\n        assert hasattr(MSSQLRunner, \"run_sql\")\n        assert not getattr(MSSQLRunner.run_sql, \"__isabstractmethod__\", False)\n\n    def test_mssql_runner_instantiation(self):\n        \"\"\"Test that MSSQLRunner can be instantiated with ODBC connection string.\"\"\"\n        from vanna.integrations.mssql import MSSQLRunner\n\n        runner = MSSQLRunner(\n            odbc_conn_str=\"Driver={SQL Server};Server=localhost;Database=test;Trusted_Connection=yes;\"\n        )\n        assert runner is not None\n\n\nclass TestPrestoRunner:\n    \"\"\"Sanity tests for PrestoRunner implementation.\"\"\"\n\n    def test_presto_runner_import(self):\n        \"\"\"Test that PrestoRunner can be imported.\"\"\"\n        from vanna.integrations.presto import PrestoRunner\n\n        assert PrestoRunner is not None\n\n    def test_presto_runner_implements_sql_runner(self):\n        \"\"\"Test that PrestoRunner implements SqlRunner interface.\"\"\"\n        from vanna.integrations.presto import PrestoRunner\n        from vanna.capabilities.sql_runner import SqlRunner\n\n        assert issubclass(PrestoRunner, SqlRunner)\n\n    def test_presto_runner_has_run_sql_method(self):\n        \"\"\"Test that PrestoRunner implements run_sql method.\"\"\"\n        from vanna.integrations.presto import PrestoRunner\n\n        assert hasattr(PrestoRunner, \"run_sql\")\n        assert not getattr(PrestoRunner.run_sql, \"__isabstractmethod__\", False)\n\n    def test_presto_runner_instantiation(self):\n        \"\"\"Test that PrestoRunner can be instantiated with required parameters.\"\"\"\n        from vanna.integrations.presto import PrestoRunner\n\n        runner = PrestoRunner(host=\"localhost\", user=\"test-user\", password=\"test-pass\")\n        assert runner is not None\n        assert runner.host == \"localhost\"\n\n\nclass TestHiveRunner:\n    \"\"\"Sanity tests for HiveRunner implementation.\"\"\"\n\n    def test_hive_runner_import(self):\n        \"\"\"Test that HiveRunner can be imported.\"\"\"\n        from vanna.integrations.hive import HiveRunner\n\n        assert HiveRunner is not None\n\n    def test_hive_runner_implements_sql_runner(self):\n        \"\"\"Test that HiveRunner implements SqlRunner interface.\"\"\"\n        from vanna.integrations.hive import HiveRunner\n        from vanna.capabilities.sql_runner import SqlRunner\n\n        assert issubclass(HiveRunner, SqlRunner)\n\n    def test_hive_runner_has_run_sql_method(self):\n        \"\"\"Test that HiveRunner implements run_sql method.\"\"\"\n        from vanna.integrations.hive import HiveRunner\n\n        assert hasattr(HiveRunner, \"run_sql\")\n        assert not getattr(HiveRunner.run_sql, \"__isabstractmethod__\", False)\n\n    def test_hive_runner_instantiation(self):\n        \"\"\"Test that HiveRunner can be instantiated with required parameters.\"\"\"\n        from vanna.integrations.hive import HiveRunner\n\n        runner = HiveRunner(host=\"localhost\", user=\"test-user\", password=\"test-pass\")\n        assert runner is not None\n        assert runner.host == \"localhost\"\n"
  },
  {
    "path": "tests/test_gemini_integration.py",
    "content": "\"\"\"\nGoogle Gemini integration tests.\n\nBasic unit tests for the Gemini LLM service integration.\nEnd-to-end tests are in test_agents.py.\n\nNote: Tests requiring API calls need GOOGLE_API_KEY environment variable.\n\"\"\"\n\nimport os\nimport pytest\nfrom vanna.core.llm import LlmRequest, LlmMessage\nfrom vanna.core.tool import ToolSchema\nfrom vanna.core.user import User\n\n\n@pytest.fixture\ndef test_user():\n    \"\"\"Test user for LLM requests.\"\"\"\n    return User(\n        id=\"test_user\",\n        username=\"test\",\n        email=\"test@example.com\",\n        group_memberships=[\"user\"],\n    )\n\n\n@pytest.mark.gemini\n@pytest.mark.asyncio\nasync def test_gemini_import():\n    \"\"\"Test that Gemini integration can be imported.\"\"\"\n    from vanna.integrations.google import GeminiLlmService\n\n    print(\"✓ GeminiLlmService imported successfully\")\n    assert GeminiLlmService is not None\n\n\n@pytest.mark.gemini\n@pytest.mark.asyncio\nasync def test_gemini_initialization_without_key():\n    \"\"\"Test that Gemini service raises error without API key.\"\"\"\n    from vanna.integrations.google import GeminiLlmService\n\n    # Clear both env vars if they exist\n    old_google_key = os.environ.pop(\"GOOGLE_API_KEY\", None)\n    old_gemini_key = os.environ.pop(\"GEMINI_API_KEY\", None)\n\n    try:\n        with pytest.raises(ValueError, match=\"Google API key is required\"):\n            llm = GeminiLlmService(model=\"gemini-2.5-pro\")\n    finally:\n        # Restore the keys if they existed\n        if old_google_key:\n            os.environ[\"GOOGLE_API_KEY\"] = old_google_key\n        if old_gemini_key:\n            os.environ[\"GEMINI_API_KEY\"] = old_gemini_key\n\n\n@pytest.mark.gemini\n@pytest.mark.asyncio\nasync def test_gemini_initialization():\n    \"\"\"Test that Gemini service can be initialized with API key.\"\"\"\n    from vanna.integrations.google import GeminiLlmService\n\n    # This test will be skipped by conftest.py if GOOGLE_API_KEY is not set\n    llm = GeminiLlmService(\n        model=\"gemini-2.5-pro\",\n        temperature=0.7,\n    )\n\n    print(f\"✓ GeminiLlmService initialized\")\n    print(f\"  Model: {llm.model_name}\")\n    print(f\"  Temperature: {llm.temperature}\")\n\n    assert llm.model_name == \"gemini-2.5-pro\"\n    assert llm.temperature == 0.7\n\n\n@pytest.mark.gemini\n@pytest.mark.asyncio\nasync def test_gemini_basic_request(test_user):\n    \"\"\"Test a basic request without tools.\"\"\"\n    from vanna.integrations.google import GeminiLlmService\n\n    llm = GeminiLlmService(model=\"gemini-2.5-pro\", temperature=0.0)\n\n    request = LlmRequest(\n        user=test_user,\n        messages=[\n            LlmMessage(role=\"user\", content=\"What is 2+2? Answer with just the number.\")\n        ],\n    )\n\n    print(f\"\\n=== Basic Request Test ===\")\n    print(f\"Sending request to Gemini...\")\n\n    response = await llm.send_request(request)\n\n    print(f\"✓ Response received\")\n    print(f\"  Content type: {type(response.content)}\")\n    print(f\"  Content: {response.content}\")\n    print(f\"  Finish reason: {response.finish_reason}\")\n    print(f\"  Tool calls: {response.tool_calls}\")\n    print(f\"  Usage: {response.usage}\")\n\n    # Verify response structure\n    assert response is not None\n    assert response.content is not None\n    assert isinstance(response.content, str)\n    assert \"4\" in response.content\n\n\n@pytest.mark.gemini\n@pytest.mark.asyncio\nasync def test_gemini_streaming_request(test_user):\n    \"\"\"Test streaming request.\"\"\"\n    from vanna.integrations.google import GeminiLlmService\n\n    llm = GeminiLlmService(model=\"gemini-2.5-pro\", temperature=0.0)\n\n    request = LlmRequest(\n        user=test_user,\n        messages=[\n            LlmMessage(role=\"user\", content=\"Count from 1 to 5, one number per line.\")\n        ],\n        stream=True,\n    )\n\n    print(f\"\\n=== Streaming Request Test ===\")\n    print(f\"Streaming from Gemini...\")\n\n    chunks = []\n    async for chunk in llm.stream_request(request):\n        chunks.append(chunk)\n        if chunk.content:\n            print(f\"  Chunk: {chunk.content}\")\n\n    print(f\"✓ Streaming completed\")\n    print(f\"  Total chunks: {len(chunks)}\")\n\n    # Verify we got chunks\n    assert len(chunks) > 0\n    # At least one chunk should have content\n    assert any(c.content for c in chunks)\n\n\n@pytest.mark.gemini\n@pytest.mark.asyncio\nasync def test_gemini_validate_tools():\n    \"\"\"Test tool validation (does not require API key for actual calls).\"\"\"\n    from vanna.integrations.google import GeminiLlmService\n\n    # For validation testing, we need to initialize but won't make API calls\n    llm = GeminiLlmService(model=\"gemini-2.5-pro\")\n\n    # Valid tool\n    valid_tool = ToolSchema(\n        name=\"test_tool\",\n        description=\"A test tool\",\n        parameters={\"type\": \"object\", \"properties\": {}},\n    )\n\n    # Invalid tool (no name)\n    invalid_tool = ToolSchema(\n        name=\"\",\n        description=\"Invalid tool\",\n        parameters={\"type\": \"object\", \"properties\": {}},\n    )\n\n    # Invalid tool (no description)\n    invalid_tool2 = ToolSchema(\n        name=\"test_tool_2\",\n        description=\"\",\n        parameters={\"type\": \"object\", \"properties\": {}},\n    )\n\n    errors = await llm.validate_tools([valid_tool])\n    assert len(errors) == 0, \"Valid tool should have no errors\"\n\n    errors = await llm.validate_tools([invalid_tool])\n    assert len(errors) > 0, \"Tool with empty name should have errors\"\n    assert any(\"name\" in e.lower() for e in errors)\n\n    errors = await llm.validate_tools([invalid_tool2])\n    assert len(errors) > 0, \"Tool with empty description should have errors\"\n    assert any(\"description\" in e.lower() for e in errors)\n\n    print(\"✓ Tool validation working correctly\")\n"
  },
  {
    "path": "tests/test_legacy_adapter.py",
    "content": "\"\"\"\nTest for LegacyVannaAdapter retrofit functionality.\n\nThis test validates that a legacy VannaBase instance can be wrapped\nwith LegacyVannaAdapter and used with the new Agents framework.\n\"\"\"\n\nimport os\nimport pytest\nfrom vanna.core.user import User\nfrom vanna.core.user.resolver import UserResolver\nfrom vanna.core.user.request_context import RequestContext\n\n\nclass SimpleUserResolver(UserResolver):\n    \"\"\"Simple user resolver for tests - returns test user or admin.\"\"\"\n\n    async def resolve_user(self, request_context: RequestContext) -> User:\n        user_email = request_context.cookies.get(\"vanna_email\", \"test@example.com\")\n\n        if user_email == \"admin@example.com\":\n            return User(id=\"admin_user\", email=user_email, group_memberships=[\"admin\"])\n\n        return User(id=user_email, email=user_email, group_memberships=[\"user\"])\n\n\n@pytest.mark.legacy\n@pytest.mark.asyncio\nasync def test_legacy_adapter_with_anthropic():\n    \"\"\"Test LegacyVannaAdapter wrapping a legacy VannaBase instance with Anthropic LLM.\"\"\"\n    from vanna import Agent, AgentConfig\n    from vanna.legacy.adapter import LegacyVannaAdapter\n    from vanna.legacy.mock import MockLLM\n    from vanna.legacy.chromadb import ChromaDB_VectorStore\n    from vanna.integrations.anthropic import AnthropicLlmService\n\n    # Create a legacy VannaBase instance (using multiple inheritance like v0.x)\n    class MyVanna(ChromaDB_VectorStore, MockLLM):\n        def __init__(self, config=None):\n            ChromaDB_VectorStore.__init__(self, config=config)\n            MockLLM.__init__(self, config=config)\n\n    vn = MyVanna()\n\n    # Connect to the Chinook database using legacy method\n    vn.connect_to_sqlite(\"https://vanna.ai/Chinook.sqlite\")\n\n    # Add some training data\n    vn.add_question_sql(\n        question=\"Who is the top artist by sales?\",\n        sql=\"SELECT a.Name, SUM(il.UnitPrice * il.Quantity) as total FROM Artist a JOIN Album al ON a.ArtistId = al.ArtistId JOIN Track t ON al.AlbumId = t.AlbumId JOIN InvoiceLine il ON t.TrackId = il.TrackId GROUP BY a.ArtistId ORDER BY total DESC LIMIT 1\",\n    )\n\n    # Wrap legacy VannaBase with LegacyVannaAdapter\n    legacy_adapter = LegacyVannaAdapter(vn)\n\n    # Create agent with new LLM service\n    api_key = os.getenv(\"ANTHROPIC_API_KEY\")\n    llm = AnthropicLlmService(api_key=api_key, model=\"claude-haiku-4-5\")\n\n    agent = Agent(\n        llm_service=llm,\n        tool_registry=legacy_adapter,  # LegacyVannaAdapter is a ToolRegistry\n        agent_memory=legacy_adapter,  # LegacyVannaAdapter implements AgentMemory\n        user_resolver=SimpleUserResolver(),\n        config=AgentConfig(),\n    )\n\n    # Test that the agent can answer a question\n    request_context = RequestContext(cookies={}, headers={})\n\n    components = []\n    async for component in agent.send_message(\n        request_context, \"Who is the top artist by sales?\"\n    ):\n        components.append(component)\n\n    # Validate we got components\n    assert len(components) > 0, \"Should receive at least one component\"\n\n    # Look for a successful response (either table or text mentioning an artist)\n    has_response = False\n    for component in components:\n        if hasattr(component, \"rich_component\") and component.rich_component:\n            has_response = True\n            break\n        if hasattr(component, \"simple_component\") and component.simple_component:\n            if (\n                hasattr(component.simple_component, \"text\")\n                and component.simple_component.text\n            ):\n                has_response = True\n                break\n\n    assert has_response, \"Should receive at least one response component\"\n\n\n@pytest.mark.legacy\n@pytest.mark.asyncio\nasync def test_legacy_adapter_memory_operations():\n    \"\"\"Test that LegacyVannaAdapter properly implements AgentMemory interface.\"\"\"\n    from vanna.legacy.adapter import LegacyVannaAdapter\n    from vanna.legacy.mock import MockLLM\n    from vanna.legacy.chromadb import ChromaDB_VectorStore\n    from vanna.core.tool import ToolContext\n    from vanna.core.user import User\n\n    # Create a legacy VannaBase instance\n    class MyVanna(ChromaDB_VectorStore, MockLLM):\n        def __init__(self, config=None):\n            ChromaDB_VectorStore.__init__(self, config=config)\n            MockLLM.__init__(self, config=config)\n\n    vn = MyVanna()\n    adapter = LegacyVannaAdapter(vn)\n\n    # Create a properly constructed tool context\n    user = User(id=\"test_user\", email=\"test@example.com\", group_memberships=[\"user\"])\n    context = ToolContext(\n        user=user,\n        conversation_id=\"test-conversation\",\n        request_id=\"test-request\",\n        agent_memory=adapter,  # Use the adapter itself as the agent_memory\n    )\n\n    # Test save_tool_usage\n    await adapter.save_tool_usage(\n        question=\"What are the total sales?\",\n        tool_name=\"run_sql\",\n        args={\"sql\": \"SELECT SUM(Total) FROM Invoice\"},\n        context=context,\n        success=True,\n    )\n\n    # Test search_similar_usage\n    results = await adapter.search_similar_usage(\n        question=\"What are sales?\", context=context, limit=5\n    )\n\n    # Should find the saved usage\n    assert len(results) > 0, \"Should find similar tool usage\"\n    assert results[0].memory.question == \"What are the total sales?\"\n    assert results[0].memory.args[\"sql\"] == \"SELECT SUM(Total) FROM Invoice\"\n\n    # Test save_text_memory\n    text_memory = await adapter.save_text_memory(\n        content=\"The Invoice table contains all sales transactions.\", context=context\n    )\n    assert text_memory.content == \"The Invoice table contains all sales transactions.\"\n\n    # Test search_text_memories\n    text_results = await adapter.search_text_memories(\n        query=\"sales\", context=context, limit=5\n    )\n    assert len(text_results) > 0, \"Should find similar text memories\"\n\n    # Test get_recent_memories (uses blank string retrieval)\n    recent = await adapter.get_recent_memories(context=context, limit=5)\n    assert isinstance(recent, list), \"Should return a list of memories\"\n\n    # Test get_recent_text_memories\n    recent_text = await adapter.get_recent_text_memories(context=context, limit=5)\n    assert isinstance(recent_text, list), \"Should return a list of text memories\"\n"
  },
  {
    "path": "tests/test_llm_context_enhancer.py",
    "content": "\"\"\"\nUnit tests for LlmContextEnhancer functionality.\n\nThese tests validate that the Agent properly calls the LlmContextEnhancer methods\nto enhance system prompts and user messages.\n\"\"\"\n\nimport pytest\nfrom typing import List\nfrom vanna.core.enhancer.base import LlmContextEnhancer\nfrom vanna.core.enhancer.default import DefaultLlmContextEnhancer\nfrom vanna.core.user import User\nfrom vanna.core.user.resolver import UserResolver\nfrom vanna.core.user.request_context import RequestContext\nfrom vanna.core.llm.models import LlmMessage\nfrom vanna.core.llm.base import LlmService\nfrom vanna.capabilities.agent_memory import (\n    AgentMemory,\n    TextMemory,\n    TextMemorySearchResult,\n)\nfrom vanna.core.tool import ToolContext\n\n\nclass MockAgentMemory(AgentMemory):\n    \"\"\"Mock AgentMemory for testing.\"\"\"\n\n    def __init__(self):\n        self.text_memories: List[TextMemory] = []\n\n    async def save_tool_usage(\n        self, question, tool_name, args, context, success=True, metadata=None\n    ):\n        pass\n\n    async def save_text_memory(self, content, context):\n        memory = TextMemory(\n            memory_id=f\"mem-{len(self.text_memories)}\", content=content, timestamp=None\n        )\n        self.text_memories.append(memory)\n        return memory\n\n    async def search_similar_usage(\n        self,\n        question,\n        context,\n        *,\n        limit=10,\n        similarity_threshold=0.7,\n        tool_name_filter=None,\n    ):\n        return []\n\n    async def search_text_memories(\n        self, query, context, *, limit=10, similarity_threshold=0.7\n    ):\n        \"\"\"Return mock search results based on stored memories.\"\"\"\n        results = []\n        for idx, memory in enumerate(self.text_memories[:limit]):\n            results.append(\n                TextMemorySearchResult(\n                    memory=memory, similarity_score=0.9 - (idx * 0.1), rank=idx + 1\n                )\n            )\n        return results\n\n    async def get_recent_memories(self, context, limit=10):\n        return []\n\n    async def get_recent_text_memories(self, context, limit=10):\n        return self.text_memories[:limit]\n\n    async def delete_by_id(self, context, memory_id):\n        return False\n\n    async def delete_text_memory(self, context, memory_id):\n        return False\n\n    async def clear_memories(self, context, tool_name=None, before_date=None):\n        return 0\n\n\nclass MockLlmService(LlmService):\n    \"\"\"Mock LLM service that records calls.\"\"\"\n\n    def __init__(self):\n        self.requests = []  # Store full request objects\n        self.next_response = \"Mock response\"\n\n    async def send_request(self, request):\n        \"\"\"Record the call and return a mock response.\"\"\"\n        from vanna.core.llm.models import LlmResponse, LlmResponseMessage\n\n        # Store the full request object\n        self.requests.append(request)\n\n        # Return a simple text response\n        return LlmResponse(\n            message=LlmResponseMessage(role=\"assistant\", content=self.next_response),\n            finish_reason=\"end_turn\",\n        )\n\n    async def stream_request(self, request):\n        \"\"\"Mock streaming - just yield a single chunk.\"\"\"\n        from vanna.core.llm.models import LlmStreamChunk\n\n        # Store the full request object\n        self.requests.append(request)\n\n        yield LlmStreamChunk(delta=self.next_response, finish_reason=\"end_turn\")\n\n    async def validate_tools(self, tools):\n        \"\"\"Mock validation - no errors.\"\"\"\n        return []\n\n\nclass SimpleUserResolver(UserResolver):\n    \"\"\"Simple user resolver for tests.\"\"\"\n\n    async def resolve_user(self, request_context: RequestContext) -> User:\n        return User(\n            id=\"test_user\", email=\"test@example.com\", group_memberships=[\"user\"]\n        )\n\n\nclass TrackingEnhancer(LlmContextEnhancer):\n    \"\"\"Custom enhancer that tracks when methods are called.\"\"\"\n\n    def __init__(self):\n        self.enhance_system_prompt_calls = []\n        self.enhance_user_messages_calls = []\n\n    async def enhance_system_prompt(\n        self, system_prompt: str, user_message: str, user: User\n    ) -> str:\n        \"\"\"Track call and add a marker to the system prompt.\"\"\"\n        self.enhance_system_prompt_calls.append(\n            {\n                \"system_prompt\": system_prompt,\n                \"user_message\": user_message,\n                \"user_id\": user.id,\n            }\n        )\n        return system_prompt + \"\\n\\n[ENHANCED_SYSTEM_PROMPT]\"\n\n    async def enhance_user_messages(\n        self, messages: List[LlmMessage], user: User\n    ) -> List[LlmMessage]:\n        \"\"\"Track call and add a marker to messages.\"\"\"\n        self.enhance_user_messages_calls.append(\n            {\"message_count\": len(messages), \"user_id\": user.id}\n        )\n        # Add a marker to the last user message\n        if messages and messages[-1].role == \"user\":\n            enhanced_messages = messages[:-1] + [\n                LlmMessage(\n                    role=\"user\",\n                    content=messages[-1].content + \" [ENHANCED_USER_MESSAGE]\",\n                )\n            ]\n            return enhanced_messages\n        return messages\n\n\n@pytest.mark.asyncio\nasync def test_custom_enhancer_system_prompt_is_called():\n    \"\"\"Test that a custom LlmContextEnhancer.enhance_system_prompt is called by the Agent.\"\"\"\n    from vanna import Agent, AgentConfig\n    from vanna.core.registry import ToolRegistry\n\n    # Create mock components\n    llm = MockLlmService()\n    tools = ToolRegistry()\n    enhancer = TrackingEnhancer()\n    agent_memory = MockAgentMemory()\n\n    # Create agent with the tracking enhancer\n    agent = Agent(\n        llm_service=llm,\n        tool_registry=tools,\n        user_resolver=SimpleUserResolver(),\n        agent_memory=agent_memory,\n        llm_context_enhancer=enhancer,\n        config=AgentConfig(),\n    )\n\n    # Send a message\n    request_context = RequestContext(cookies={}, headers={})\n    components = []\n    async for component in agent.send_message(request_context, \"Hello, world!\"):\n        components.append(component)\n\n    # Verify enhance_system_prompt was called\n    assert len(enhancer.enhance_system_prompt_calls) == 1, (\n        \"enhance_system_prompt should be called exactly once\"\n    )\n    call = enhancer.enhance_system_prompt_calls[0]\n    assert call[\"user_message\"] == \"Hello, world!\", (\n        \"Should pass the user message to enhancer\"\n    )\n    assert call[\"user_id\"] == \"test_user\", \"Should pass the correct user\"\n    assert \"system_prompt\" in call, \"Should pass the system prompt\"\n\n    # Verify the enhanced system prompt was sent to the LLM\n    assert len(llm.requests) >= 1, \"LLM should be called at least once\"\n    first_request = llm.requests[0]\n    assert first_request.system_prompt is not None, \"Should have a system prompt\"\n    assert \"[ENHANCED_SYSTEM_PROMPT]\" in first_request.system_prompt, (\n        f\"System prompt should contain enhancement marker. Got: {first_request.system_prompt}\"\n    )\n\n\n@pytest.mark.asyncio\nasync def test_custom_enhancer_user_messages_is_called():\n    \"\"\"Test that a custom LlmContextEnhancer.enhance_user_messages is called by the Agent.\"\"\"\n    from vanna import Agent, AgentConfig\n    from vanna.core.registry import ToolRegistry\n\n    # Create mock components\n    llm = MockLlmService()\n    tools = ToolRegistry()\n    enhancer = TrackingEnhancer()\n    agent_memory = MockAgentMemory()\n\n    # Create agent with the tracking enhancer\n    agent = Agent(\n        llm_service=llm,\n        tool_registry=tools,\n        user_resolver=SimpleUserResolver(),\n        agent_memory=agent_memory,\n        llm_context_enhancer=enhancer,\n        config=AgentConfig(),\n    )\n\n    # Send a message\n    request_context = RequestContext(cookies={}, headers={})\n    components = []\n    async for component in agent.send_message(request_context, \"Test message\"):\n        components.append(component)\n\n    # Verify enhance_user_messages was called\n    assert len(enhancer.enhance_user_messages_calls) >= 1\n    call = enhancer.enhance_user_messages_calls[0]\n    assert call[\"user_id\"] == \"test_user\"\n    assert call[\"message_count\"] > 0\n\n    # Verify the enhanced user message was sent to the LLM\n    assert len(llm.requests) >= 1\n    first_request = llm.requests[0]\n    user_messages = [m for m in first_request.messages if m.role == \"user\"]\n    assert len(user_messages) >= 1, \"Should have at least one user message\"\n    assert \"[ENHANCED_USER_MESSAGE]\" in user_messages[0].content, (\n        f\"User message should be enhanced. Got: {user_messages[0].content}\"\n    )\n\n\n@pytest.mark.asyncio\nasync def test_default_enhancer_with_agent_memory():\n    \"\"\"Test that DefaultLlmContextEnhancer properly enhances system prompt with memories.\"\"\"\n    from vanna import Agent, AgentConfig\n    from vanna.core.registry import ToolRegistry\n\n    # Create mock components\n    llm = MockLlmService()\n    tools = ToolRegistry()\n    agent_memory = MockAgentMemory()\n\n    # Add some test memories\n    user = User(id=\"test_user\", email=\"test@example.com\", group_memberships=[\"user\"])\n    context = ToolContext(\n        user=user, conversation_id=\"test\", request_id=\"test\", agent_memory=agent_memory\n    )\n\n    await agent_memory.save_text_memory(\n        \"The database has a users table with columns: id, name, email\", context\n    )\n    await agent_memory.save_text_memory(\n        \"The products table contains: product_id, name, price, category\", context\n    )\n\n    # Create default enhancer with agent memory\n    enhancer = DefaultLlmContextEnhancer(agent_memory=agent_memory)\n\n    # Create agent\n    agent = Agent(\n        llm_service=llm,\n        tool_registry=tools,\n        user_resolver=SimpleUserResolver(),\n        agent_memory=agent_memory,\n        llm_context_enhancer=enhancer,\n        config=AgentConfig(),\n    )\n\n    # Send a message that should trigger memory retrieval\n    request_context = RequestContext(cookies={}, headers={})\n    components = []\n    async for component in agent.send_message(\n        request_context, \"Show me the database schema\"\n    ):\n        components.append(component)\n\n    # Verify that the DefaultLlmContextEnhancer added memory context to the system prompt\n    assert len(llm.requests) >= 1, \"LLM should be called\"\n    first_request = llm.requests[0]\n\n    # The DefaultLlmContextEnhancer should add \"Relevant Context from Memory\" to system prompt\n    assert first_request.system_prompt is not None, \"Should have a system prompt\"\n    assert \"Relevant Context from Memory\" in first_request.system_prompt, (\n        f\"System prompt should include memory context. Got: {first_request.system_prompt}\"\n    )\n\n    # Should contain one or both of the memories we added\n    assert (\n        \"users table\" in first_request.system_prompt\n        or \"products table\" in first_request.system_prompt\n    ), (\n        f\"System prompt should contain our test memories. Got: {first_request.system_prompt}\"\n    )\n\n\n@pytest.mark.asyncio\nasync def test_default_enhancer_without_agent_memory():\n    \"\"\"Test that DefaultLlmContextEnhancer works without agent memory (no enhancement).\"\"\"\n    from vanna import Agent, AgentConfig\n    from vanna.core.registry import ToolRegistry\n\n    # Create mock components\n    llm = MockLlmService()\n    tools = ToolRegistry()\n\n    # Create default enhancer without agent memory\n    enhancer = DefaultLlmContextEnhancer(agent_memory=None)\n    agent_memory = MockAgentMemory()\n\n    # Create agent\n    agent = Agent(\n        llm_service=llm,\n        tool_registry=tools,\n        user_resolver=SimpleUserResolver(),\n        agent_memory=agent_memory,\n        llm_context_enhancer=enhancer,\n        config=AgentConfig(),\n    )\n\n    # Send a message\n    request_context = RequestContext(cookies={}, headers={})\n    components = []\n    async for component in agent.send_message(request_context, \"Hello\"):\n        components.append(component)\n\n    # Verify the system prompt does NOT include memory context (since enhancer has no agent_memory)\n    assert len(llm.requests) >= 1, \"LLM should be called\"\n    first_request = llm.requests[0]\n\n    # The system prompt should exist but NOT contain memory context\n    if first_request.system_prompt:\n        assert \"Relevant Context from Memory\" not in first_request.system_prompt, (\n            \"Should not add memory context when enhancer has no agent_memory\"\n        )\n\n\n@pytest.mark.asyncio\nasync def test_no_enhancer_means_no_enhancement():\n    \"\"\"Test that when no enhancer is provided, no enhancement occurs.\"\"\"\n    from vanna import Agent, AgentConfig\n    from vanna.core.registry import ToolRegistry\n\n    # Create mock components\n    llm = MockLlmService()\n    tools = ToolRegistry()\n    agent_memory = MockAgentMemory()\n\n    # Create agent WITHOUT an enhancer (should use default no-op behavior)\n    agent = Agent(\n        llm_service=llm,\n        tool_registry=tools,\n        user_resolver=SimpleUserResolver(),\n        agent_memory=agent_memory,\n        config=AgentConfig(),\n    )\n\n    # Send a message\n    request_context = RequestContext(cookies={}, headers={})\n    components = []\n    async for component in agent.send_message(request_context, \"Hello\"):\n        components.append(component)\n\n    # Verify basic system prompt exists but has no enhancements\n    assert len(llm.requests) >= 1, \"LLM should be called\"\n    first_request = llm.requests[0]\n\n    # Should be the basic system prompt with no enhancements\n    if first_request.system_prompt:\n        assert \"Relevant Context from Memory\" not in first_request.system_prompt, (\n            \"Should not add memory context when no enhancer is provided\"\n        )\n"
  },
  {
    "path": "tests/test_memory_tools.py",
    "content": "\"\"\"\nTests for agent memory tools, including UI feature access control.\n\"\"\"\n\nimport pytest\nimport uuid\nfrom vanna.tools.agent_memory import (\n    SearchSavedCorrectToolUsesTool,\n    SearchSavedCorrectToolUsesParams,\n)\nfrom vanna.core.tool import ToolContext\nfrom vanna.core.user import User\nfrom vanna.core.agent.config import UiFeature\nfrom vanna.integrations.local.agent_memory import DemoAgentMemory\nfrom vanna.core.rich_component import ComponentType\n\n\n@pytest.fixture\ndef demo_agent_memory():\n    \"\"\"Create a demo agent memory instance.\"\"\"\n    return DemoAgentMemory(max_items=100)\n\n\n@pytest.fixture\ndef admin_user():\n    \"\"\"Create an admin user.\"\"\"\n    return User(id=\"admin\", email=\"admin@example.com\", group_memberships=[\"admin\"])\n\n\n@pytest.fixture\ndef regular_user():\n    \"\"\"Create a regular user.\"\"\"\n    return User(id=\"user\", email=\"user@example.com\", group_memberships=[\"user\"])\n\n\n@pytest.fixture\ndef search_tool():\n    \"\"\"Create a search tool instance.\"\"\"\n    return SearchSavedCorrectToolUsesTool()\n\n\nclass TestMemoryToolDetailedResults:\n    \"\"\"Test memory tool detailed results feature.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_admin_sees_detailed_results(\n        self, search_tool, demo_agent_memory, admin_user\n    ):\n        \"\"\"Test that admin users see detailed memory results in a collapsible card.\"\"\"\n        # Create context with admin user and feature enabled\n        context = ToolContext(\n            user=admin_user,\n            conversation_id=str(uuid.uuid4()),\n            request_id=str(uuid.uuid4()),\n            agent_memory=demo_agent_memory,\n            metadata={\n                \"ui_features_available\": [\n                    UiFeature.UI_FEATURE_SHOW_MEMORY_DETAILED_RESULTS\n                ]\n            },\n        )\n\n        # Save some memories\n        await demo_agent_memory.save_tool_usage(\n            question=\"What is the total sales?\",\n            tool_name=\"run_sql\",\n            args={\"query\": \"SELECT SUM(total) FROM invoices\"},\n            context=context,\n            success=True,\n        )\n\n        # Search for similar patterns\n        search_params = SearchSavedCorrectToolUsesParams(\n            question=\"What are the total sales?\", limit=10, similarity_threshold=0.5\n        )\n\n        result = await search_tool.execute(context, search_params)\n\n        # Verify result\n        assert result.success is True\n        assert result.ui_component is not None\n        assert result.ui_component.rich_component is not None\n\n        # Check that it's a CardComponent (detailed view)\n        assert result.ui_component.rich_component.type == ComponentType.CARD\n\n        # Check collapsible properties\n        card = result.ui_component.rich_component\n        assert card.collapsible is True\n        assert card.collapsed is True  # Should start collapsed\n\n        # Verify content includes detailed information\n        assert \"Retrieved memories passed to LLM\" in card.content\n        assert \"run_sql\" in card.content\n        assert \"similarity:\" in card.content.lower()\n        assert \"Question:\" in card.content\n        assert \"Arguments:\" in card.content\n\n    @pytest.mark.asyncio\n    async def test_non_admin_sees_simple_status(\n        self, search_tool, demo_agent_memory, regular_user\n    ):\n        \"\"\"Test that non-admin users see simple status message.\"\"\"\n        # Create context with regular user (no detailed results feature)\n        context = ToolContext(\n            user=regular_user,\n            conversation_id=str(uuid.uuid4()),\n            request_id=str(uuid.uuid4()),\n            agent_memory=demo_agent_memory,\n            metadata={\"ui_features_available\": []},  # No detailed results feature\n        )\n\n        # Save some memories\n        await demo_agent_memory.save_tool_usage(\n            question=\"What is the total sales?\",\n            tool_name=\"run_sql\",\n            args={\"query\": \"SELECT SUM(total) FROM invoices\"},\n            context=context,\n            success=True,\n        )\n\n        # Search for similar patterns\n        search_params = SearchSavedCorrectToolUsesParams(\n            question=\"What are the total sales?\", limit=10, similarity_threshold=0.5\n        )\n\n        result = await search_tool.execute(context, search_params)\n\n        # Verify result\n        assert result.success is True\n        assert result.ui_component is not None\n        assert result.ui_component.rich_component is not None\n\n        # Check that it's a StatusBarUpdateComponent (simple view)\n        assert (\n            result.ui_component.rich_component.type == ComponentType.STATUS_BAR_UPDATE\n        )\n\n        # Verify it shows success message\n        status = result.ui_component.rich_component\n        assert status.status == \"success\"\n        assert \"similar pattern\" in status.message.lower()\n\n    @pytest.mark.asyncio\n    async def test_detailed_results_include_all_memory_fields(\n        self, search_tool, demo_agent_memory, admin_user\n    ):\n        \"\"\"Test that detailed results include all relevant memory fields.\"\"\"\n        # Create context with admin user and feature enabled\n        context = ToolContext(\n            user=admin_user,\n            conversation_id=str(uuid.uuid4()),\n            request_id=str(uuid.uuid4()),\n            agent_memory=demo_agent_memory,\n            metadata={\n                \"ui_features_available\": [\n                    UiFeature.UI_FEATURE_SHOW_MEMORY_DETAILED_RESULTS\n                ]\n            },\n        )\n\n        # Save a memory\n        await demo_agent_memory.save_tool_usage(\n            question=\"Show me customer names\",\n            tool_name=\"run_sql\",\n            args={\"query\": \"SELECT name FROM customers\"},\n            context=context,\n            success=True,\n        )\n\n        # Search for it\n        search_params = SearchSavedCorrectToolUsesParams(\n            question=\"Show customer names\", limit=10, similarity_threshold=0.3\n        )\n\n        result = await search_tool.execute(context, search_params)\n\n        # Verify detailed content\n        card = result.ui_component.rich_component\n        content = card.content\n\n        # Check for all expected fields\n        assert \"Question:\" in content\n        assert \"Show me customer names\" in content\n        assert \"Arguments:\" in content\n        assert \"run_sql\" in content\n        assert \"similarity:\" in content.lower()\n\n        # Timestamp and ID should be included if available\n        # (DemoAgentMemory might not set these, but the code should handle them)\n\n    @pytest.mark.asyncio\n    async def test_no_results_works_for_both_admin_and_user(\n        self, search_tool, demo_agent_memory, admin_user, regular_user\n    ):\n        \"\"\"Test that admin sees card with 0 results while regular user sees status bar.\"\"\"\n        # Test with admin\n        admin_context = ToolContext(\n            user=admin_user,\n            conversation_id=str(uuid.uuid4()),\n            request_id=str(uuid.uuid4()),\n            agent_memory=demo_agent_memory,\n            metadata={\n                \"ui_features_available\": [\n                    UiFeature.UI_FEATURE_SHOW_MEMORY_DETAILED_RESULTS\n                ]\n            },\n        )\n\n        search_params = SearchSavedCorrectToolUsesParams(\n            question=\"This query will not match anything\",\n            limit=10,\n            similarity_threshold=0.99,\n        )\n\n        admin_result = await search_tool.execute(admin_context, search_params)\n\n        assert admin_result.success is True\n        assert \"No similar tool usage patterns found\" in admin_result.result_for_llm\n        # Admin should see a card showing 0 results\n        assert admin_result.ui_component.rich_component.type == ComponentType.CARD\n        assert \"0 Results\" in admin_result.ui_component.rich_component.title\n        assert admin_result.ui_component.rich_component.collapsible is True\n\n        # Test with regular user\n        user_context = ToolContext(\n            user=regular_user,\n            conversation_id=str(uuid.uuid4()),\n            request_id=str(uuid.uuid4()),\n            agent_memory=demo_agent_memory,\n            metadata={\"ui_features_available\": []},\n        )\n\n        user_result = await search_tool.execute(user_context, search_params)\n\n        assert user_result.success is True\n        assert \"No similar tool usage patterns found\" in user_result.result_for_llm\n        # Regular user should see a status bar update\n        assert (\n            user_result.ui_component.rich_component.type\n            == ComponentType.STATUS_BAR_UPDATE\n        )\n\n    @pytest.mark.asyncio\n    async def test_llm_result_same_for_admin_and_user(\n        self, search_tool, demo_agent_memory, admin_user, regular_user\n    ):\n        \"\"\"Test that the LLM receives the same information regardless of UI feature access.\"\"\"\n        # Save a memory\n        admin_context = ToolContext(\n            user=admin_user,\n            conversation_id=str(uuid.uuid4()),\n            request_id=str(uuid.uuid4()),\n            agent_memory=demo_agent_memory,\n            metadata={\n                \"ui_features_available\": [\n                    UiFeature.UI_FEATURE_SHOW_MEMORY_DETAILED_RESULTS\n                ]\n            },\n        )\n\n        await demo_agent_memory.save_tool_usage(\n            question=\"Count all records\",\n            tool_name=\"run_sql\",\n            args={\"query\": \"SELECT COUNT(*) FROM table\"},\n            context=admin_context,\n            success=True,\n        )\n\n        search_params = SearchSavedCorrectToolUsesParams(\n            question=\"Count records\", limit=10, similarity_threshold=0.3\n        )\n\n        # Get admin result\n        admin_result = await search_tool.execute(admin_context, search_params)\n\n        # Get regular user result\n        user_context = ToolContext(\n            user=regular_user,\n            conversation_id=str(uuid.uuid4()),\n            request_id=str(uuid.uuid4()),\n            agent_memory=demo_agent_memory,\n            metadata={\"ui_features_available\": []},\n        )\n\n        user_result = await search_tool.execute(user_context, search_params)\n\n        # Both should have the same result_for_llm\n        assert admin_result.result_for_llm == user_result.result_for_llm\n        assert \"Found\" in admin_result.result_for_llm\n        assert \"similar tool usage pattern\" in admin_result.result_for_llm\n\n\nif __name__ == \"__main__\":\n    pytest.main([__file__, \"-v\"])\n"
  },
  {
    "path": "tests/test_ollama_direct.py",
    "content": "\"\"\"\nDirect Ollama integration tests to diagnose and verify the integration.\n\nThese tests check each aspect of the Ollama integration separately.\n\"\"\"\n\nimport pytest\nfrom vanna.core.llm import LlmRequest, LlmMessage\nfrom vanna.core.tool import ToolSchema\nfrom vanna.core.user import User\n\n\n@pytest.fixture\ndef test_user():\n    \"\"\"Test user for LLM requests.\"\"\"\n    return User(\n        id=\"test_user\",\n        username=\"test\",\n        email=\"test@example.com\",\n        group_memberships=[\"user\"],\n    )\n\n\n@pytest.mark.ollama\n@pytest.mark.asyncio\nasync def test_ollama_import():\n    \"\"\"Test that Ollama integration can be imported.\"\"\"\n    try:\n        from vanna.integrations.ollama import OllamaLlmService\n\n        print(\"✓ OllamaLlmService imported successfully\")\n        assert OllamaLlmService is not None\n    except ImportError as e:\n        pytest.fail(f\"Failed to import OllamaLlmService: {e}\")\n\n\n@pytest.mark.ollama\n@pytest.mark.asyncio\nasync def test_ollama_initialization():\n    \"\"\"Test that Ollama service can be initialized.\"\"\"\n    from vanna.integrations.ollama import OllamaLlmService\n\n    try:\n        llm = OllamaLlmService(\n            model=\"llama3.2\", host=\"http://localhost:11434\", temperature=0.0\n        )\n\n        print(f\"✓ OllamaLlmService initialized\")\n        print(f\"  Model: {llm.model}\")\n        print(f\"  Host: {llm.host}\")\n        print(f\"  Temperature: {llm.temperature}\")\n        print(f\"  Context window: {llm.num_ctx}\")\n\n        assert llm.model == \"llama3.2\"\n        assert llm.host == \"http://localhost:11434\"\n\n    except Exception as e:\n        pytest.fail(f\"Failed to initialize OllamaLlmService: {e}\")\n\n\n@pytest.mark.ollama\n@pytest.mark.asyncio\nasync def test_ollama_basic_request(test_user):\n    \"\"\"Test a basic request without tools.\"\"\"\n    from vanna.integrations.ollama import OllamaLlmService\n\n    llm = OllamaLlmService(model=\"llama3.2\", temperature=0.0)\n\n    request = LlmRequest(\n        user=test_user,\n        messages=[\n            LlmMessage(role=\"user\", content=\"What is 2+2? Answer with just the number.\")\n        ],\n    )\n\n    print(f\"\\n=== Basic Request Test ===\")\n    print(f\"Sending request to Ollama...\")\n\n    try:\n        response = await llm.send_request(request)\n\n        print(f\"✓ Response received\")\n        print(f\"  Content type: {type(response.content)}\")\n        print(f\"  Content: {response.content}\")\n        print(f\"  Finish reason: {response.finish_reason}\")\n        print(f\"  Tool calls: {response.tool_calls}\")\n        print(f\"  Usage: {response.usage}\")\n\n        # Verify response structure\n        assert response is not None\n        assert response.content is not None\n        assert isinstance(response.content, str)\n\n    except Exception as e:\n        print(f\"❌ Error during request: {e}\")\n        import traceback\n\n        traceback.print_exc()\n        raise\n\n\n@pytest.mark.ollama\n@pytest.mark.asyncio\nasync def test_ollama_pydantic_response(test_user):\n    \"\"\"Test that the response is a valid Pydantic model.\"\"\"\n    from vanna.integrations.ollama import OllamaLlmService\n    from vanna.core.llm import LlmResponse\n\n    llm = OllamaLlmService(model=\"llama3.2\", temperature=0.0)\n\n    request = LlmRequest(\n        user=test_user, messages=[LlmMessage(role=\"user\", content=\"Say 'hello'\")]\n    )\n\n    print(f\"\\n=== Pydantic Validation Test ===\")\n\n    try:\n        response = await llm.send_request(request)\n\n        # Verify it's a Pydantic model\n        assert isinstance(response, LlmResponse)\n        print(f\"✓ Response is LlmResponse instance\")\n\n        # Test model_dump\n        dumped = response.model_dump()\n        print(f\"✓ model_dump() works: {dumped}\")\n\n        # Test model_dump_json\n        json_str = response.model_dump_json()\n        print(f\"✓ model_dump_json() works: {json_str[:100]}...\")\n\n        # Test reconstruction\n        reconstructed = LlmResponse(**dumped)\n        print(f\"✓ Reconstruction works\")\n        assert reconstructed.content == response.content\n\n    except Exception as e:\n        print(f\"❌ Pydantic error: {e}\")\n        import traceback\n\n        traceback.print_exc()\n        raise\n\n\n@pytest.mark.ollama\n@pytest.mark.asyncio\nasync def test_ollama_streaming(test_user):\n    \"\"\"Test streaming responses.\"\"\"\n    from vanna.integrations.ollama import OllamaLlmService\n    from vanna.core.llm import LlmStreamChunk\n\n    llm = OllamaLlmService(model=\"llama3.2\", temperature=0.0)\n\n    request = LlmRequest(\n        user=test_user, messages=[LlmMessage(role=\"user\", content=\"Count from 1 to 3.\")]\n    )\n\n    print(f\"\\n=== Streaming Test ===\")\n\n    try:\n        chunks = []\n        content_parts = []\n\n        async for chunk in llm.stream_request(request):\n            chunks.append(chunk)\n\n            # Verify chunk is correct type\n            assert isinstance(chunk, LlmStreamChunk)\n\n            if chunk.content:\n                content_parts.append(chunk.content)\n                print(f\"  Chunk {len(chunks)}: {chunk.content!r}\")\n\n        full_content = \"\".join(content_parts)\n\n        print(f\"✓ Streaming completed\")\n        print(f\"  Total chunks: {len(chunks)}\")\n        print(f\"  Full content: {full_content}\")\n        print(f\"  Final finish_reason: {chunks[-1].finish_reason}\")\n\n        assert len(chunks) > 0\n        assert chunks[-1].finish_reason is not None\n\n    except Exception as e:\n        print(f\"❌ Streaming error: {e}\")\n        import traceback\n\n        traceback.print_exc()\n        raise\n\n\n@pytest.mark.ollama\n@pytest.mark.asyncio\nasync def test_ollama_tool_calling_attempt(test_user):\n    \"\"\"Test tool calling with Ollama (may not work with all models).\"\"\"\n    from vanna.integrations.ollama import OllamaLlmService\n\n    llm = OllamaLlmService(model=\"llama3.2\", temperature=0.0)\n\n    tools = [\n        ToolSchema(\n            name=\"get_weather\",\n            description=\"Get the current weather for a location\",\n            parameters={\n                \"type\": \"object\",\n                \"properties\": {\n                    \"location\": {\"type\": \"string\", \"description\": \"City name\"}\n                },\n                \"required\": [\"location\"],\n            },\n        )\n    ]\n\n    request = LlmRequest(\n        user=test_user,\n        system_prompt=\"You are a helpful assistant. When asked about weather, use the get_weather tool.\",\n        messages=[\n            LlmMessage(role=\"user\", content=\"What's the weather in San Francisco?\")\n        ],\n        tools=tools,\n    )\n\n    print(f\"\\n=== Tool Calling Test ===\")\n    print(f\"Model: {llm.model}\")\n    print(f\"Tools provided: {[t.name for t in tools]}\")\n\n    try:\n        response = await llm.send_request(request)\n\n        print(f\"\\nResponse:\")\n        print(f\"  Content: {response.content}\")\n        print(f\"  Tool calls: {response.tool_calls}\")\n        print(f\"  Finish reason: {response.finish_reason}\")\n\n        if response.tool_calls:\n            print(f\"\\n✓ Tool calling works!\")\n            print(f\"  Number of tool calls: {len(response.tool_calls)}\")\n            for i, tc in enumerate(response.tool_calls):\n                print(f\"  Tool call {i + 1}:\")\n                print(f\"    ID: {tc.id}\")\n                print(f\"    Name: {tc.name}\")\n                print(f\"    Arguments: {tc.arguments}\")\n                print(f\"    Arguments type: {type(tc.arguments)}\")\n        else:\n            print(f\"\\n⚠️  Model returned text instead of tool calls\")\n            print(f\"  This is expected for models without tool calling support\")\n            print(f\"  Try using llama3.1:8b or mistral for better tool calling\")\n\n        # Test passes either way - we're just diagnosing\n        assert response is not None\n\n    except Exception as e:\n        print(f\"❌ Tool calling error: {e}\")\n        import traceback\n\n        traceback.print_exc()\n        raise\n\n\n@pytest.mark.ollama\n@pytest.mark.asyncio\nasync def test_ollama_payload_building(test_user):\n    \"\"\"Test that the payload is built correctly.\"\"\"\n    from vanna.integrations.ollama import OllamaLlmService\n\n    llm = OllamaLlmService(model=\"llama3.2\", temperature=0.5, num_ctx=4096)\n\n    tools = [\n        ToolSchema(\n            name=\"test_tool\",\n            description=\"A test tool\",\n            parameters={\"type\": \"object\", \"properties\": {}},\n        )\n    ]\n\n    request = LlmRequest(\n        user=test_user,\n        system_prompt=\"You are a test assistant.\",\n        messages=[LlmMessage(role=\"user\", content=\"Hello\")],\n        tools=tools,\n    )\n\n    print(f\"\\n=== Payload Building Test ===\")\n\n    try:\n        # Access the internal method to inspect payload\n        payload = llm._build_payload(request)\n\n        print(f\"Built payload:\")\n        print(f\"  Model: {payload.get('model')}\")\n        print(f\"  Messages count: {len(payload.get('messages', []))}\")\n        print(f\"  First message: {payload.get('messages', [{}])[0]}\")\n        print(f\"  Options: {payload.get('options')}\")\n        print(f\"  Tools: {payload.get('tools')}\")\n\n        # Verify payload structure\n        assert payload[\"model\"] == \"llama3.2\"\n        assert len(payload[\"messages\"]) == 2  # system + user\n        assert payload[\"messages\"][0][\"role\"] == \"system\"\n        assert payload[\"messages\"][1][\"role\"] == \"user\"\n        assert payload[\"options\"][\"temperature\"] == 0.5\n        assert payload[\"options\"][\"num_ctx\"] == 4096\n        assert \"tools\" in payload\n        assert len(payload[\"tools\"]) == 1\n\n        print(f\"✓ Payload structure is correct\")\n\n    except Exception as e:\n        print(f\"❌ Payload building error: {e}\")\n        import traceback\n\n        traceback.print_exc()\n        raise\n"
  },
  {
    "path": "tests/test_tool_permissions.py",
    "content": "\"\"\"\nTests for tool access control and permissions.\n\"\"\"\n\nimport pytest\nfrom pydantic import BaseModel, Field\nfrom typing import Type, TypeVar, Union\n\nfrom vanna.core.tool import (\n    Tool,\n    ToolContext,\n    ToolResult,\n    ToolCall,\n    ToolRejection,\n    ToolSchema,\n)\nfrom vanna.core.user import User\nfrom vanna.core.registry import ToolRegistry\nfrom vanna.components import UiComponent, SimpleTextComponent\nfrom vanna.integrations.local.agent_memory import DemoAgentMemory\n\nT = TypeVar(\"T\")\n\n\nclass SimpleToolArgs(BaseModel):\n    \"\"\"Simple args for testing.\"\"\"\n\n    message: str = Field(description=\"A message\")\n\n\nclass MockTool(Tool[SimpleToolArgs]):\n    \"\"\"Mock tool for testing.\"\"\"\n\n    def __init__(self, tool_name: str = \"mock_tool\"):\n        self._tool_name = tool_name\n\n    @property\n    def name(self) -> str:\n        return self._tool_name\n\n    @property\n    def description(self) -> str:\n        return f\"A mock tool for testing: {self._tool_name}\"\n\n    def get_args_schema(self) -> Type[SimpleToolArgs]:\n        return SimpleToolArgs\n\n    async def execute(self, context: ToolContext, args: SimpleToolArgs) -> ToolResult:\n        return ToolResult(\n            success=True, result_for_llm=f\"Mock tool executed: {args.message}\"\n        )\n\n\n@pytest.fixture\ndef agent_memory():\n    \"\"\"Agent memory for testing.\"\"\"\n    return DemoAgentMemory(max_items=100)\n\n\n@pytest.fixture\ndef admin_user():\n    \"\"\"Admin user with admin group.\"\"\"\n    return User(\n        id=\"admin_1\",\n        username=\"admin\",\n        email=\"admin@example.com\",\n        group_memberships=[\"admin\", \"user\"],\n    )\n\n\n@pytest.fixture\ndef regular_user():\n    \"\"\"Regular user with user group.\"\"\"\n    return User(\n        id=\"user_1\",\n        username=\"user\",\n        email=\"user@example.com\",\n        group_memberships=[\"user\"],\n    )\n\n\n@pytest.fixture\ndef analyst_user():\n    \"\"\"Analyst user with analyst group.\"\"\"\n    return User(\n        id=\"analyst_1\",\n        username=\"analyst\",\n        email=\"analyst@example.com\",\n        group_memberships=[\"analyst\", \"user\"],\n    )\n\n\n@pytest.fixture\ndef guest_user():\n    \"\"\"Guest user with no groups.\"\"\"\n    return User(\n        id=\"guest_1\", username=\"guest\", email=\"guest@example.com\", group_memberships=[]\n    )\n\n\n@pytest.mark.asyncio\nasync def test_tool_access_empty_groups_allows_all(regular_user, agent_memory):\n    \"\"\"Test that empty access_groups allows all users.\"\"\"\n    print(\"\\n=== Empty Access Groups Test ===\")\n\n    registry = ToolRegistry()\n    tool = MockTool(\"public_tool\")\n\n    # Register with empty access groups\n    registry.register_local_tool(tool, access_groups=[])\n\n    # Create context\n    context = ToolContext(\n        user=regular_user,\n        conversation_id=\"test_conv\",\n        request_id=\"test_req\",\n        agent_memory=agent_memory,\n    )\n\n    # Execute tool\n    tool_call = ToolCall(\n        id=\"call_1\", name=\"public_tool\", arguments={\"message\": \"hello\"}\n    )\n\n    result = await registry.execute(tool_call, context)\n\n    print(f\"✓ Tool executed with empty access groups\")\n    print(f\"  Success: {result.success}\")\n\n    assert result.success is True\n    assert \"Mock tool executed\" in result.result_for_llm\n\n\n@pytest.mark.asyncio\nasync def test_tool_access_granted_matching_group(admin_user, agent_memory):\n    \"\"\"Test that user with matching group can access tool.\"\"\"\n    print(\"\\n=== Access Granted Test ===\")\n\n    registry = ToolRegistry()\n    tool = MockTool(\"admin_tool\")\n\n    # Register with admin-only access\n    registry.register_local_tool(tool, access_groups=[\"admin\"])\n\n    context = ToolContext(\n        user=admin_user,\n        conversation_id=\"test_conv\",\n        request_id=\"test_req\",\n        agent_memory=agent_memory,\n    )\n\n    tool_call = ToolCall(\n        id=\"call_1\", name=\"admin_tool\", arguments={\"message\": \"admin action\"}\n    )\n\n    result = await registry.execute(tool_call, context)\n\n    print(f\"✓ Admin user accessed admin-only tool\")\n    print(f\"  User groups: {admin_user.group_memberships}\")\n    print(f\"  Tool access groups: ['admin']\")\n    print(f\"  Success: {result.success}\")\n\n    assert result.success is True\n\n\n@pytest.mark.asyncio\nasync def test_tool_access_denied_no_matching_group(regular_user, agent_memory):\n    \"\"\"Test that user without matching group cannot access tool.\"\"\"\n    print(\"\\n=== Access Denied Test ===\")\n\n    registry = ToolRegistry()\n    tool = MockTool(\"admin_tool\")\n\n    # Register with admin-only access\n    registry.register_local_tool(tool, access_groups=[\"admin\"])\n\n    context = ToolContext(\n        user=regular_user,\n        conversation_id=\"test_conv\",\n        request_id=\"test_req\",\n        agent_memory=agent_memory,\n    )\n\n    tool_call = ToolCall(\n        id=\"call_1\", name=\"admin_tool\", arguments={\"message\": \"should fail\"}\n    )\n\n    result = await registry.execute(tool_call, context)\n\n    print(f\"✓ Regular user denied access to admin-only tool\")\n    print(f\"  User groups: {regular_user.group_memberships}\")\n    print(f\"  Tool access groups: ['admin']\")\n    print(f\"  Success: {result.success}\")\n    print(f\"  Error: {result.error}\")\n\n    assert result.success is False\n    assert \"Insufficient group access\" in result.result_for_llm\n    assert \"admin_tool\" in result.result_for_llm\n\n\n@pytest.mark.asyncio\nasync def test_tool_access_multiple_allowed_groups(\n    analyst_user, admin_user, regular_user, agent_memory\n):\n    \"\"\"Test tool with multiple allowed groups.\"\"\"\n    print(\"\\n=== Multiple Allowed Groups Test ===\")\n\n    registry = ToolRegistry()\n    tool = MockTool(\"data_tool\")\n\n    # Allow both admin and analyst groups\n    registry.register_local_tool(tool, access_groups=[\"admin\", \"analyst\"])\n\n    # Test analyst can access\n    analyst_context = ToolContext(\n        user=analyst_user,\n        conversation_id=\"test_conv\",\n        request_id=\"test_req_1\",\n        agent_memory=agent_memory,\n    )\n\n    tool_call = ToolCall(id=\"call_1\", name=\"data_tool\", arguments={\"message\": \"test\"})\n    result = await registry.execute(tool_call, analyst_context)\n\n    print(f\"✓ Analyst accessed tool\")\n    assert result.success is True\n\n    # Test admin can access\n    admin_context = ToolContext(\n        user=admin_user,\n        conversation_id=\"test_conv\",\n        request_id=\"test_req_2\",\n        agent_memory=agent_memory,\n    )\n\n    result = await registry.execute(tool_call, admin_context)\n\n    print(f\"✓ Admin accessed tool\")\n    assert result.success is True\n\n    # Test regular user cannot access\n    user_context = ToolContext(\n        user=regular_user,\n        conversation_id=\"test_conv\",\n        request_id=\"test_req_3\",\n        agent_memory=agent_memory,\n    )\n\n    result = await registry.execute(tool_call, user_context)\n\n    print(f\"✓ Regular user denied access\")\n    assert result.success is False\n\n\n@pytest.mark.asyncio\nasync def test_tool_access_guest_user_denied(guest_user, agent_memory):\n    \"\"\"Test that guest user with no groups cannot access restricted tools.\"\"\"\n    print(\"\\n=== Guest User Denied Test ===\")\n\n    registry = ToolRegistry()\n    tool = MockTool(\"restricted_tool\")\n\n    registry.register_local_tool(tool, access_groups=[\"user\"])\n\n    context = ToolContext(\n        user=guest_user,\n        conversation_id=\"test_conv\",\n        request_id=\"test_req\",\n        agent_memory=agent_memory,\n    )\n\n    tool_call = ToolCall(\n        id=\"call_1\", name=\"restricted_tool\", arguments={\"message\": \"test\"}\n    )\n    result = await registry.execute(tool_call, context)\n\n    print(f\"✓ Guest user with no groups denied access\")\n    print(f\"  User groups: {guest_user.group_memberships}\")\n    print(f\"  Tool access groups: ['user']\")\n\n    assert result.success is False\n    assert \"Insufficient group access\" in result.result_for_llm\n\n\n@pytest.mark.asyncio\nasync def test_get_schemas_filters_by_user(admin_user, regular_user):\n    \"\"\"Test that get_schemas only returns tools accessible to user.\"\"\"\n    print(\"\\n=== Schema Filtering Test ===\")\n\n    registry = ToolRegistry()\n\n    # Register tools with different access levels\n    registry.register_local_tool(MockTool(\"public_tool\"), access_groups=[])\n    registry.register_local_tool(MockTool(\"admin_tool\"), access_groups=[\"admin\"])\n    registry.register_local_tool(MockTool(\"user_tool\"), access_groups=[\"user\"])\n\n    # Admin user should see all tools\n    admin_schemas = await registry.get_schemas(admin_user)\n    admin_tool_names = [s.name for s in admin_schemas]\n\n    print(f\"✓ Admin user schemas: {admin_tool_names}\")\n    assert \"public_tool\" in admin_tool_names\n    assert \"admin_tool\" in admin_tool_names\n    assert \"user_tool\" in admin_tool_names\n    assert len(admin_tool_names) == 3\n\n    # Regular user should only see public and user tools\n    user_schemas = await registry.get_schemas(regular_user)\n    user_tool_names = [s.name for s in user_schemas]\n\n    print(f\"✓ Regular user schemas: {user_tool_names}\")\n    assert \"public_tool\" in user_tool_names\n    assert \"user_tool\" in user_tool_names\n    assert \"admin_tool\" not in user_tool_names\n    assert len(user_tool_names) == 2\n\n\n@pytest.mark.asyncio\nasync def test_tool_not_found(agent_memory):\n    \"\"\"Test execution of non-existent tool.\"\"\"\n    print(\"\\n=== Tool Not Found Test ===\")\n\n    registry = ToolRegistry()\n\n    user = User(id=\"user\", username=\"user\", group_memberships=[\"user\"])\n    context = ToolContext(\n        user=user,\n        conversation_id=\"test_conv\",\n        request_id=\"test_req\",\n        agent_memory=agent_memory,\n    )\n\n    tool_call = ToolCall(\n        id=\"call_1\", name=\"nonexistent_tool\", arguments={\"message\": \"test\"}\n    )\n\n    result = await registry.execute(tool_call, context)\n\n    print(f\"✓ Non-existent tool handled gracefully\")\n    print(f\"  Error: {result.error}\")\n\n    assert result.success is False\n    assert \"not found\" in result.result_for_llm.lower()\n\n\n@pytest.mark.asyncio\nasync def test_duplicate_tool_registration():\n    \"\"\"Test that registering the same tool twice raises error.\"\"\"\n    print(\"\\n=== Duplicate Registration Test ===\")\n\n    registry = ToolRegistry()\n    tool = MockTool(\"duplicate_tool\")\n\n    # First registration should succeed\n    registry.register_local_tool(tool, access_groups=[])\n    print(f\"✓ First registration succeeded\")\n\n    # Second registration should fail\n    with pytest.raises(ValueError, match=\"already registered\"):\n        registry.register_local_tool(tool, access_groups=[])\n\n    print(f\"✓ Duplicate registration properly rejected\")\n\n\n@pytest.mark.asyncio\nasync def test_tool_access_group_intersection(admin_user, agent_memory):\n    \"\"\"Test that access is granted on ANY matching group (not all groups).\"\"\"\n    print(\"\\n=== Group Intersection Test ===\")\n\n    registry = ToolRegistry()\n    tool = MockTool(\"multi_group_tool\")\n\n    # Tool requires either admin OR analyst\n    registry.register_local_tool(tool, access_groups=[\"admin\", \"analyst\"])\n\n    # User has admin (but not analyst) - should still have access\n    context = ToolContext(\n        user=admin_user,\n        conversation_id=\"test_conv\",\n        request_id=\"test_req\",\n        agent_memory=agent_memory,\n    )\n\n    tool_call = ToolCall(\n        id=\"call_1\", name=\"multi_group_tool\", arguments={\"message\": \"test\"}\n    )\n    result = await registry.execute(tool_call, context)\n\n    print(f\"✓ User with ONE matching group granted access\")\n    print(f\"  User groups: {admin_user.group_memberships}\")\n    print(f\"  Tool requires: ['admin', 'analyst']\")\n    print(f\"  Intersection: {set(admin_user.group_memberships) & {'admin', 'analyst'}}\")\n\n    assert result.success is True\n\n\n@pytest.mark.asyncio\nasync def test_list_tools():\n    \"\"\"Test listing all registered tools.\"\"\"\n    print(\"\\n=== List Tools Test ===\")\n\n    registry = ToolRegistry()\n\n    registry.register_local_tool(MockTool(\"tool1\"), access_groups=[])\n    registry.register_local_tool(MockTool(\"tool2\"), access_groups=[\"admin\"])\n    registry.register_local_tool(MockTool(\"tool3\"), access_groups=[\"user\"])\n\n    tools = await registry.list_tools()\n\n    print(f\"✓ Listed {len(tools)} tools\")\n    print(f\"  Tools: {tools}\")\n\n    assert len(tools) == 3\n    assert \"tool1\" in tools\n    assert \"tool2\" in tools\n    assert \"tool3\" in tools\n\n\n# ============================================================================\n# transform_args Tests\n# ============================================================================\n\n\n@pytest.mark.asyncio\nasync def test_transform_args_default_no_transformation(regular_user, agent_memory):\n    \"\"\"Test that default transform_args implementation returns args unchanged.\"\"\"\n    print(\"\\n=== Default transform_args (NoOp) Test ===\")\n\n    registry = ToolRegistry()\n    tool = MockTool(\"test_tool\")\n    registry.register_local_tool(tool, access_groups=[])\n\n    context = ToolContext(\n        user=regular_user,\n        conversation_id=\"test_conv\",\n        request_id=\"test_req\",\n        agent_memory=agent_memory,\n    )\n\n    # Test the transform_args method directly\n    original_args = SimpleToolArgs(message=\"original message\")\n    transformed_args = await registry.transform_args(\n        tool=tool,\n        args=original_args,\n        user=regular_user,\n        context=context,\n    )\n\n    print(f\"✓ Default transform_args returns args unchanged\")\n    print(f\"  Original: {original_args.message}\")\n    print(f\"  Transformed: {transformed_args.message}\")\n\n    assert transformed_args == original_args\n    assert transformed_args.message == \"original message\"\n    assert not isinstance(transformed_args, ToolRejection)\n\n\nclass CustomTransformRegistry(ToolRegistry):\n    \"\"\"Custom registry that modifies arguments.\"\"\"\n\n    async def transform_args(\n        self,\n        tool: Tool[T],\n        args: T,\n        user: User,\n        context: ToolContext,\n    ) -> Union[T, ToolRejection]:\n        \"\"\"Custom transform that appends user info to message.\"\"\"\n        if isinstance(args, SimpleToolArgs):\n            # Modify the args by appending user info\n            modified_args = SimpleToolArgs(\n                message=f\"{args.message} [user: {user.username}]\"\n            )\n            return modified_args\n        return args\n\n\n@pytest.mark.asyncio\nasync def test_transform_args_custom_modification(regular_user, agent_memory):\n    \"\"\"Test custom transform_args that modifies arguments.\"\"\"\n    print(\"\\n=== Custom transform_args Modification Test ===\")\n\n    registry = CustomTransformRegistry()\n    tool = MockTool(\"test_tool\")\n    registry.register_local_tool(tool, access_groups=[])\n\n    context = ToolContext(\n        user=regular_user,\n        conversation_id=\"test_conv\",\n        request_id=\"test_req\",\n        agent_memory=agent_memory,\n    )\n\n    # Execute tool - args should be transformed\n    tool_call = ToolCall(\n        id=\"call_1\",\n        name=\"test_tool\",\n        arguments={\"message\": \"hello\"},\n    )\n\n    result = await registry.execute(tool_call, context)\n\n    print(f\"✓ Tool executed with transformed args\")\n    print(f\"  Result: {result.result_for_llm}\")\n\n    assert result.success is True\n    assert \"hello [user: user]\" in result.result_for_llm\n    assert \"Mock tool executed\" in result.result_for_llm\n\n\nclass RejectionRegistry(ToolRegistry):\n    \"\"\"Custom registry that rejects certain arguments.\"\"\"\n\n    async def transform_args(\n        self,\n        tool: Tool[T],\n        args: T,\n        user: User,\n        context: ToolContext,\n    ) -> Union[T, ToolRejection]:\n        \"\"\"Reject args containing 'forbidden' keyword.\"\"\"\n        if isinstance(args, SimpleToolArgs):\n            if \"forbidden\" in args.message.lower():\n                return ToolRejection(\n                    reason=f\"Message contains forbidden keyword. User '{user.username}' is not allowed to use this word.\"\n                )\n        return args\n\n\n@pytest.mark.asyncio\nasync def test_transform_args_rejection(regular_user, agent_memory):\n    \"\"\"Test transform_args returning ToolRejection.\"\"\"\n    print(\"\\n=== transform_args Rejection Test ===\")\n\n    registry = RejectionRegistry()\n    tool = MockTool(\"test_tool\")\n    registry.register_local_tool(tool, access_groups=[])\n\n    context = ToolContext(\n        user=regular_user,\n        conversation_id=\"test_conv\",\n        request_id=\"test_req\",\n        agent_memory=agent_memory,\n    )\n\n    # Execute tool with forbidden word\n    tool_call = ToolCall(\n        id=\"call_1\",\n        name=\"test_tool\",\n        arguments={\"message\": \"this is forbidden\"},\n    )\n\n    result = await registry.execute(tool_call, context)\n\n    print(f\"✓ Tool execution rejected by transform_args\")\n    print(f\"  Error: {result.error}\")\n\n    assert result.success is False\n    assert \"forbidden keyword\" in result.result_for_llm\n    assert \"not allowed\" in result.result_for_llm\n    assert \"user\" in result.result_for_llm\n\n\n@pytest.mark.asyncio\nasync def test_transform_args_allows_approved_content(regular_user, agent_memory):\n    \"\"\"Test that transform_args allows approved content.\"\"\"\n    print(\"\\n=== transform_args Allows Approved Content Test ===\")\n\n    registry = RejectionRegistry()\n    tool = MockTool(\"test_tool\")\n    registry.register_local_tool(tool, access_groups=[])\n\n    context = ToolContext(\n        user=regular_user,\n        conversation_id=\"test_conv\",\n        request_id=\"test_req\",\n        agent_memory=agent_memory,\n    )\n\n    # Execute tool with approved message\n    tool_call = ToolCall(\n        id=\"call_1\",\n        name=\"test_tool\",\n        arguments={\"message\": \"this is allowed\"},\n    )\n\n    result = await registry.execute(tool_call, context)\n\n    print(f\"✓ Tool execution allowed for approved content\")\n    print(f\"  Result: {result.result_for_llm}\")\n\n    assert result.success is True\n    assert \"Mock tool executed\" in result.result_for_llm\n    assert \"this is allowed\" in result.result_for_llm\n\n\nclass RowLevelSecurityRegistry(ToolRegistry):\n    \"\"\"Custom registry that applies row-level security transformations.\"\"\"\n\n    async def transform_args(\n        self,\n        tool: Tool[T],\n        args: T,\n        user: User,\n        context: ToolContext,\n    ) -> Union[T, ToolRejection]:\n        \"\"\"Apply RLS by modifying SQL queries based on user groups.\"\"\"\n        if isinstance(args, SimpleToolArgs):\n            # Simulate SQL query transformation for RLS\n            if \"SELECT\" in args.message.upper():\n                # Add WHERE clause based on user groups\n                if \"admin\" in user.group_memberships:\n                    # Admins see everything - no modification needed\n                    return args\n                elif \"analyst\" in user.group_memberships:\n                    # Analysts see filtered data\n                    modified_message = args.message + \" WHERE department='analytics'\"\n                    return SimpleToolArgs(message=modified_message)\n                else:\n                    # Regular users see even more restricted data\n                    modified_message = args.message + f\" WHERE user_id='{user.id}'\"\n                    return SimpleToolArgs(message=modified_message)\n        return args\n\n\n@pytest.mark.asyncio\nasync def test_transform_args_row_level_security(\n    admin_user, analyst_user, regular_user, agent_memory\n):\n    \"\"\"Test transform_args implementing row-level security.\"\"\"\n    print(\"\\n=== Row-Level Security Test ===\")\n\n    registry = RowLevelSecurityRegistry()\n    tool = MockTool(\"sql_tool\")\n    registry.register_local_tool(tool, access_groups=[])\n\n    base_query = \"SELECT * FROM users\"\n\n    # Test admin user - no modification\n    admin_context = ToolContext(\n        user=admin_user,\n        conversation_id=\"test_conv\",\n        request_id=\"test_req_1\",\n        agent_memory=agent_memory,\n    )\n\n    tool_call = ToolCall(\n        id=\"call_1\",\n        name=\"sql_tool\",\n        arguments={\"message\": base_query},\n    )\n\n    result = await registry.execute(tool_call, admin_context)\n    print(f\"✓ Admin query: {result.result_for_llm}\")\n    assert result.success is True\n    assert \"WHERE\" not in result.result_for_llm\n\n    # Test analyst user - department filter\n    analyst_context = ToolContext(\n        user=analyst_user,\n        conversation_id=\"test_conv\",\n        request_id=\"test_req_2\",\n        agent_memory=agent_memory,\n    )\n\n    result = await registry.execute(tool_call, analyst_context)\n    print(f\"✓ Analyst query: {result.result_for_llm}\")\n    assert result.success is True\n    assert \"WHERE department='analytics'\" in result.result_for_llm\n\n    # Test regular user - user_id filter\n    regular_context = ToolContext(\n        user=regular_user,\n        conversation_id=\"test_conv\",\n        request_id=\"test_req_3\",\n        agent_memory=agent_memory,\n    )\n\n    result = await registry.execute(tool_call, regular_context)\n    print(f\"✓ Regular user query: {result.result_for_llm}\")\n    assert result.success is True\n    assert f\"WHERE user_id='{regular_user.id}'\" in result.result_for_llm\n\n\n@pytest.mark.asyncio\nasync def test_transform_args_called_during_execution(regular_user, agent_memory):\n    \"\"\"Test that transform_args is called during tool execution flow.\"\"\"\n    print(\"\\n=== transform_args Called During Execution Test ===\")\n\n    class InstrumentedRegistry(ToolRegistry):\n        def __init__(self):\n            super().__init__()\n            self.transform_called = False\n            self.transform_call_count = 0\n\n        async def transform_args(self, tool, args, user, context):\n            self.transform_called = True\n            self.transform_call_count += 1\n            return await super().transform_args(tool, args, user, context)\n\n    registry = InstrumentedRegistry()\n    tool = MockTool(\"test_tool\")\n    registry.register_local_tool(tool, access_groups=[])\n\n    context = ToolContext(\n        user=regular_user,\n        conversation_id=\"test_conv\",\n        request_id=\"test_req\",\n        agent_memory=agent_memory,\n    )\n\n    tool_call = ToolCall(\n        id=\"call_1\",\n        name=\"test_tool\",\n        arguments={\"message\": \"test\"},\n    )\n\n    result = await registry.execute(tool_call, context)\n\n    print(f\"✓ transform_args was called during execution\")\n    print(f\"  Called: {registry.transform_called}\")\n    print(f\"  Call count: {registry.transform_call_count}\")\n\n    assert result.success is True\n    assert registry.transform_called is True\n    assert registry.transform_call_count == 1\n\n\n@pytest.mark.asyncio\nasync def test_transform_args_receives_correct_parameters(regular_user, agent_memory):\n    \"\"\"Test that transform_args receives correct parameters.\"\"\"\n    print(\"\\n=== transform_args Parameter Validation Test ===\")\n\n    class ParameterCheckRegistry(ToolRegistry):\n        def __init__(self):\n            super().__init__()\n            self.received_tool = None\n            self.received_args = None\n            self.received_user = None\n            self.received_context = None\n\n        async def transform_args(self, tool, args, user, context):\n            self.received_tool = tool\n            self.received_args = args\n            self.received_user = user\n            self.received_context = context\n            return await super().transform_args(tool, args, user, context)\n\n    registry = ParameterCheckRegistry()\n    tool = MockTool(\"test_tool\")\n    registry.register_local_tool(tool, access_groups=[])\n\n    context = ToolContext(\n        user=regular_user,\n        conversation_id=\"test_conv_123\",\n        request_id=\"test_req_456\",\n        agent_memory=agent_memory,\n    )\n\n    tool_call = ToolCall(\n        id=\"call_1\",\n        name=\"test_tool\",\n        arguments={\"message\": \"test message\"},\n    )\n\n    result = await registry.execute(tool_call, context)\n\n    print(f\"✓ transform_args received correct parameters\")\n    print(f\"  Tool name: {registry.received_tool.name}\")\n    print(f\"  Args message: {registry.received_args.message}\")\n    print(f\"  User: {registry.received_user.username}\")\n    print(f\"  Context conv_id: {registry.received_context.conversation_id}\")\n\n    assert result.success is True\n    assert registry.received_tool is not None\n    assert registry.received_tool.name == \"test_tool\"\n    assert registry.received_args is not None\n    assert registry.received_args.message == \"test message\"\n    assert registry.received_user is not None\n    assert registry.received_user.id == regular_user.id\n    assert registry.received_context is not None\n    assert registry.received_context.conversation_id == \"test_conv_123\"\n\n\n@pytest.mark.asyncio\nasync def test_transform_args_called_during_agent_send_message():\n    \"\"\"Test that transform_args is called during Agent.send_message workflow.\"\"\"\n    print(\"\\n=== transform_args in Agent.send_message Test ===\")\n\n    # Import necessary components\n    from vanna import Agent\n    from vanna.core.user.resolver import UserResolver\n    from vanna.core.user.request_context import RequestContext\n    from vanna.core.llm import LlmService, LlmRequest, LlmResponse, LlmMessage\n    from vanna.core.agent.config import AgentConfig\n\n    # Create a custom registry that tracks transform_args calls\n    class InstrumentedAgentRegistry(ToolRegistry):\n        def __init__(self):\n            super().__init__()\n            self.transform_calls = []\n\n        async def transform_args(self, tool, args, user, context):\n            # Track that transform_args was called\n            self.transform_calls.append(\n                {\n                    \"tool_name\": tool.name,\n                    \"args\": args,\n                    \"user_id\": user.id,\n                    \"conversation_id\": context.conversation_id,\n                }\n            )\n            # Apply a transformation to verify it's used\n            if isinstance(args, SimpleToolArgs):\n                modified_args = SimpleToolArgs(message=f\"{args.message} [transformed]\")\n                return modified_args\n            return args\n\n    # Create a simple user resolver\n    class TestUserResolver(UserResolver):\n        async def resolve_user(self, request_context: RequestContext) -> User:\n            return User(\n                id=\"test_user_123\",\n                username=\"test_user\",\n                email=\"test@example.com\",\n                group_memberships=[\"user\"],\n            )\n\n    # Create a mock LLM service that calls a tool\n    class MockLlmService(LlmService):\n        async def send_request(self, request: LlmRequest) -> LlmResponse:\n            # Return a response that calls our mock tool\n            return LlmResponse(\n                content=\"I'll use the mock tool\",\n                tool_calls=[\n                    ToolCall(\n                        id=\"call_123\",\n                        name=\"mock_tool\",\n                        arguments={\"message\": \"test message\"},\n                    )\n                ],\n                raw_response=None,\n            )\n\n        async def stream_request(self, request: LlmRequest):\n            # Yield a single chunk with tool call\n            from vanna.core.llm import LlmStreamChunk\n\n            yield LlmStreamChunk(\n                content=\"I'll use the mock tool\",\n                tool_calls=[\n                    ToolCall(\n                        id=\"call_123\",\n                        name=\"mock_tool\",\n                        arguments={\"message\": \"test message\"},\n                    )\n                ],\n                raw_chunk=None,\n            )\n\n        async def validate_tools(self, tools: list[ToolSchema]) -> None:\n            # No validation needed for this test\n            pass\n\n    # Set up the agent\n    instrumented_registry = InstrumentedAgentRegistry()\n    tool = MockTool(\"mock_tool\")\n    instrumented_registry.register_local_tool(tool, access_groups=[])\n\n    agent_memory = DemoAgentMemory(max_items=100)\n\n    agent = Agent(\n        llm_service=MockLlmService(),\n        tool_registry=instrumented_registry,\n        user_resolver=TestUserResolver(),\n        agent_memory=agent_memory,\n        config=AgentConfig(),\n    )\n\n    # Send a message through the agent\n    request_context = RequestContext(cookies={}, headers={})\n    components = []\n\n    async for component in agent.send_message(request_context, \"test message\"):\n        components.append(component)\n\n    print(f\"✓ Agent.send_message completed\")\n    print(f\"  Transform calls: {len(instrumented_registry.transform_calls)}\")\n    print(f\"  Components received: {len(components)}\")\n\n    # Verify that transform_args was called\n    assert len(instrumented_registry.transform_calls) > 0, (\n        \"transform_args should be called during Agent.send_message\"\n    )\n\n    # Check the first transform call\n    first_call = instrumented_registry.transform_calls[0]\n    assert first_call[\"tool_name\"] == \"mock_tool\"\n    assert first_call[\"user_id\"] == \"test_user_123\"\n    assert first_call[\"conversation_id\"] is not None\n\n    print(f\"  ✓ transform_args was called with correct parameters\")\n    print(f\"    Tool: {first_call['tool_name']}\")\n    print(f\"    User: {first_call['user_id']}\")\n    print(\n        f\"    Total transform_args calls: {len(instrumented_registry.transform_calls)}\"\n    )\n\n    # Verify that the args passed to transform_args have the expected message\n    assert first_call[\"args\"].message == \"test message\"\n    print(f\"    Original args message: {first_call['args'].message}\")\n\n    print(\n        f\"  ✓ All checks passed - transform_args is called during Agent.send_message workflow\"\n    )\n"
  },
  {
    "path": "tests/test_workflow.py",
    "content": "\"\"\"\nTests for the default workflow handler, including memory display and deletion.\n\"\"\"\n\nimport pytest\nimport uuid\nfrom datetime import datetime\nfrom vanna.core.workflow.default import DefaultWorkflowHandler\nfrom vanna.core.user import User\nfrom vanna.core.user.resolver import UserResolver\nfrom vanna.core.user.request_context import RequestContext\nfrom vanna.core.storage import Conversation\nfrom vanna.core.tool import ToolContext\nfrom vanna.capabilities.agent_memory import ToolMemory, TextMemory\nfrom vanna.integrations.local.agent_memory import DemoAgentMemory\nfrom vanna.core.rich_component import ComponentType\n\n\nclass SimpleUserResolver(UserResolver):\n    \"\"\"Simple user resolver for tests.\"\"\"\n\n    async def resolve_user(self, request_context: RequestContext) -> User:\n        return User(\n            id=\"test_user\", email=\"test@example.com\", group_memberships=[\"user\"]\n        )\n\n\nclass MockAgent:\n    \"\"\"Mock agent for testing workflow handlers.\"\"\"\n\n    def __init__(self, agent_memory=None, tool_registry=None):\n        self.agent_memory = agent_memory\n        self.tool_registry = tool_registry or MockToolRegistry()\n\n\nclass MockToolRegistry:\n    \"\"\"Mock tool registry for testing.\"\"\"\n\n    async def get_schemas(self, user):\n        \"\"\"Return mock tool schemas.\"\"\"\n        return []\n\n\nclass MockConversation:\n    \"\"\"Mock conversation for testing.\"\"\"\n\n    def __init__(self, conversation_id=None):\n        self.id = conversation_id or str(uuid.uuid4())\n\n\n@pytest.fixture\ndef test_user():\n    \"\"\"Create a test user.\"\"\"\n    return User(id=\"test_user\", email=\"test@example.com\", group_memberships=[\"user\"])\n\n\n@pytest.fixture\ndef admin_test_user():\n    \"\"\"Create an admin test user for tests that require admin access.\"\"\"\n    return User(id=\"admin_user\", email=\"admin@example.com\", group_memberships=[\"admin\"])\n\n\n@pytest.fixture\ndef test_conversation():\n    \"\"\"Create a test conversation.\"\"\"\n    return MockConversation()\n\n\n@pytest.fixture\ndef workflow_handler():\n    \"\"\"Create a workflow handler instance.\"\"\"\n    return DefaultWorkflowHandler()\n\n\n@pytest.fixture\ndef agent_with_memory():\n    \"\"\"Create a mock agent with memory.\"\"\"\n    memory = DemoAgentMemory(max_items=100)\n    return MockAgent(agent_memory=memory)\n\n\n@pytest.fixture\ndef agent_without_memory():\n    \"\"\"Create a mock agent without memory.\"\"\"\n    return MockAgent(agent_memory=None)\n\n\nclass TestWorkflowCommands:\n    \"\"\"Test basic workflow command handling.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_help_command(\n        self, workflow_handler, agent_with_memory, test_user, test_conversation\n    ):\n        \"\"\"Test that /help command returns help message (non-admin view).\"\"\"\n        result = await workflow_handler.try_handle(\n            agent_with_memory, test_user, test_conversation, \"/help\"\n        )\n\n        assert result.should_skip_llm is True\n        assert len(result.components) > 0\n\n        # Check that help content includes basic commands\n        help_component = result.components[0]\n        content = help_component.rich_component.content\n        assert \"/help\" in content\n        # Admin commands should NOT be visible to non-admin users\n        assert \"Admin Commands\" not in content\n        assert \"/status\" not in content\n        assert \"/memories\" not in content\n        assert \"/delete\" not in content\n\n    @pytest.mark.asyncio\n    async def test_status_command(\n        self,\n        workflow_handler,\n        agent_with_memory,\n        admin_test_user,\n        test_conversation,\n    ):\n        \"\"\"Test that /status command returns status information (admin only).\"\"\"\n        result = await workflow_handler.try_handle(\n            agent_with_memory, admin_test_user, test_conversation, \"/status\"\n        )\n\n        assert result.should_skip_llm is True\n        assert len(result.components) > 0\n\n    @pytest.mark.asyncio\n    async def test_memories_command(\n        self,\n        workflow_handler,\n        agent_with_memory,\n        admin_test_user,\n        test_conversation,\n    ):\n        \"\"\"Test that /memories command returns memory list (admin only).\"\"\"\n        result = await workflow_handler.try_handle(\n            agent_with_memory, admin_test_user, test_conversation, \"/memories\"\n        )\n\n        assert result.should_skip_llm is True\n        assert len(result.components) > 0\n\n    @pytest.mark.asyncio\n    async def test_unknown_command(\n        self, workflow_handler, agent_with_memory, test_user, test_conversation\n    ):\n        \"\"\"Test that unknown commands are passed to LLM.\"\"\"\n        result = await workflow_handler.try_handle(\n            agent_with_memory, test_user, test_conversation, \"What is the weather?\"\n        )\n\n        assert result.should_skip_llm is False\n\n\nclass TestMemoriesView:\n    \"\"\"Test the memories view functionality.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_memories_no_agent_memory(\n        self, workflow_handler, agent_without_memory, test_user, test_conversation\n    ):\n        \"\"\"Test memories view when agent has no memory capability.\"\"\"\n        result = await workflow_handler._get_recent_memories(\n            agent_without_memory, test_user, test_conversation\n        )\n\n        assert result.should_skip_llm is True\n        assert len(result.components) == 1\n\n        content = result.components[0].rich_component.content\n        assert \"No Memory System\" in content\n\n    @pytest.mark.asyncio\n    async def test_memories_empty(\n        self, workflow_handler, agent_with_memory, test_user, test_conversation\n    ):\n        \"\"\"Test memories view when no memories exist.\"\"\"\n        result = await workflow_handler._get_recent_memories(\n            agent_with_memory, test_user, test_conversation\n        )\n\n        assert result.should_skip_llm is True\n        assert len(result.components) == 1\n\n        content = result.components[0].rich_component.content\n        assert \"No recent memories found\" in content\n\n    @pytest.mark.asyncio\n    async def test_memories_with_tool_memories(\n        self, workflow_handler, agent_with_memory, test_user, test_conversation\n    ):\n        \"\"\"Test memories view displays tool memories correctly.\"\"\"\n        # Create a context and add some tool memories\n        context = ToolContext(\n            user=test_user,\n            conversation_id=test_conversation.id,\n            request_id=str(uuid.uuid4()),\n            agent_memory=agent_with_memory.agent_memory,\n        )\n\n        # Add tool memories\n        await agent_with_memory.agent_memory.save_tool_usage(\n            question=\"What is the total sales?\",\n            tool_name=\"run_sql\",\n            args={\"query\": \"SELECT SUM(total) FROM invoices\"},\n            context=context,\n            success=True,\n        )\n\n        await agent_with_memory.agent_memory.save_tool_usage(\n            question=\"Show me customer names\",\n            tool_name=\"run_sql\",\n            args={\"query\": \"SELECT name FROM customers\"},\n            context=context,\n            success=True,\n        )\n\n        # Get memories view\n        result = await workflow_handler._get_recent_memories(\n            agent_with_memory, test_user, test_conversation\n        )\n\n        assert result.should_skip_llm is True\n        assert len(result.components) > 1  # Header + at least one memory card\n\n        # Check header\n        header = result.components[0].rich_component.content\n        assert \"Recent Memories\" in header\n        assert \"2\" in header  # Should show count\n\n        # Check that we have tool memory section\n        found_tool_section = False\n        for component in result.components:\n            if hasattr(component.rich_component, \"content\"):\n                if \"Tool Memories\" in component.rich_component.content:\n                    found_tool_section = True\n                    break\n\n        assert found_tool_section\n\n    @pytest.mark.asyncio\n    async def test_memories_with_text_memories(\n        self, workflow_handler, agent_with_memory, test_user, test_conversation\n    ):\n        \"\"\"Test memories view displays text memories correctly.\"\"\"\n        # Create a context and add some text memories\n        context = ToolContext(\n            user=test_user,\n            conversation_id=test_conversation.id,\n            request_id=str(uuid.uuid4()),\n            agent_memory=agent_with_memory.agent_memory,\n        )\n\n        # Add text memories\n        await agent_with_memory.agent_memory.save_text_memory(\n            content=\"Remember to always check the data quality\",\n            context=context,\n        )\n\n        await agent_with_memory.agent_memory.save_text_memory(\n            content=\"The fiscal year starts in April\",\n            context=context,\n        )\n\n        # Get memories view\n        result = await workflow_handler._get_recent_memories(\n            agent_with_memory, test_user, test_conversation\n        )\n\n        assert result.should_skip_llm is True\n        assert len(result.components) > 1  # Header + at least one memory card\n\n        # Check that we have text memory section\n        found_text_section = False\n        for component in result.components:\n            if hasattr(component.rich_component, \"content\"):\n                if \"Text Memories\" in component.rich_component.content:\n                    found_text_section = True\n                    break\n\n        assert found_text_section\n\n    @pytest.mark.asyncio\n    async def test_memories_with_both_types(\n        self, workflow_handler, agent_with_memory, test_user, test_conversation\n    ):\n        \"\"\"Test memories view displays both tool and text memories.\"\"\"\n        # Create a context\n        context = ToolContext(\n            user=test_user,\n            conversation_id=test_conversation.id,\n            request_id=str(uuid.uuid4()),\n            agent_memory=agent_with_memory.agent_memory,\n        )\n\n        # Add tool memory\n        await agent_with_memory.agent_memory.save_tool_usage(\n            question=\"What is the total sales?\",\n            tool_name=\"run_sql\",\n            args={\"query\": \"SELECT SUM(total) FROM invoices\"},\n            context=context,\n            success=True,\n        )\n\n        # Add text memory\n        await agent_with_memory.agent_memory.save_text_memory(\n            content=\"Important note about the data\",\n            context=context,\n        )\n\n        # Get memories view\n        result = await workflow_handler._get_recent_memories(\n            agent_with_memory, test_user, test_conversation\n        )\n\n        assert result.should_skip_llm is True\n\n        # Should have header + text section + text cards + tool section + tool cards\n        assert len(result.components) >= 4\n\n        # Check both sections exist\n        content_str = str(\n            [\n                c.rich_component.content\n                if hasattr(c.rich_component, \"content\")\n                else str(c.rich_component)\n                for c in result.components\n            ]\n        )\n        assert \"Text Memories\" in content_str\n        assert \"Tool Memories\" in content_str\n\n    @pytest.mark.asyncio\n    async def test_memories_have_delete_buttons(\n        self, workflow_handler, agent_with_memory, test_user, test_conversation\n    ):\n        \"\"\"Test that memory cards include delete buttons.\"\"\"\n        # Create a context and add a memory\n        context = ToolContext(\n            user=test_user,\n            conversation_id=test_conversation.id,\n            request_id=str(uuid.uuid4()),\n            agent_memory=agent_with_memory.agent_memory,\n        )\n\n        await agent_with_memory.agent_memory.save_tool_usage(\n            question=\"Test question\",\n            tool_name=\"test_tool\",\n            args={\"param\": \"value\"},\n            context=context,\n            success=True,\n        )\n\n        # Get memories view\n        result = await workflow_handler._get_recent_memories(\n            agent_with_memory, test_user, test_conversation\n        )\n\n        # Find a card component with actions\n        found_delete_button = False\n        for component in result.components:\n            if (\n                hasattr(component.rich_component, \"actions\")\n                and component.rich_component.actions\n            ):\n                for action in component.rich_component.actions:\n                    if \"/delete\" in action.get(\"action\", \"\"):\n                        found_delete_button = True\n                        break\n\n        assert found_delete_button, \"Should have delete button in memory cards\"\n\n\nclass TestMemoryDeletion:\n    \"\"\"Test memory deletion functionality.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_delete_no_agent_memory(\n        self, workflow_handler, agent_without_memory, test_user, test_conversation\n    ):\n        \"\"\"Test delete command when agent has no memory capability.\"\"\"\n        result = await workflow_handler._delete_memory(\n            agent_without_memory, test_user, test_conversation, \"test-id\"\n        )\n\n        assert result.should_skip_llm is True\n        content = result.components[0].rich_component.content\n        assert \"No Memory System\" in content\n\n    @pytest.mark.asyncio\n    async def test_delete_no_id_provided(\n        self, workflow_handler, agent_with_memory, test_user, test_conversation\n    ):\n        \"\"\"Test delete command without memory ID.\"\"\"\n        result = await workflow_handler._delete_memory(\n            agent_with_memory, test_user, test_conversation, \"\"\n        )\n\n        assert result.should_skip_llm is True\n        content = result.components[0].rich_component.content\n        assert \"Invalid Command\" in content\n        assert \"memory_id\" in content\n\n    @pytest.mark.asyncio\n    async def test_delete_nonexistent_memory(\n        self, workflow_handler, agent_with_memory, test_user, test_conversation\n    ):\n        \"\"\"Test deleting a memory that doesn't exist.\"\"\"\n        result = await workflow_handler._delete_memory(\n            agent_with_memory, test_user, test_conversation, \"nonexistent-id\"\n        )\n\n        assert result.should_skip_llm is True\n        content = result.components[0].rich_component.content\n        assert \"Memory Not Found\" in content\n\n    @pytest.mark.asyncio\n    async def test_delete_tool_memory_success(\n        self, workflow_handler, agent_with_memory, test_user, test_conversation\n    ):\n        \"\"\"Test successfully deleting a tool memory.\"\"\"\n        # Create a context and add a memory\n        context = ToolContext(\n            user=test_user,\n            conversation_id=test_conversation.id,\n            request_id=str(uuid.uuid4()),\n            agent_memory=agent_with_memory.agent_memory,\n        )\n\n        await agent_with_memory.agent_memory.save_tool_usage(\n            question=\"Test question\",\n            tool_name=\"test_tool\",\n            args={\"param\": \"value\"},\n            context=context,\n            success=True,\n        )\n\n        # Get the memory ID\n        memories = await agent_with_memory.agent_memory.get_recent_memories(\n            context, limit=1\n        )\n        assert len(memories) == 1\n        memory_id = memories[0].memory_id\n\n        # Delete the memory\n        result = await workflow_handler._delete_memory(\n            agent_with_memory, test_user, test_conversation, memory_id\n        )\n\n        assert result.should_skip_llm is True\n        content = result.components[0].rich_component.content\n        assert \"Memory Deleted\" in content\n        assert memory_id in content\n\n        # Verify memory is gone\n        memories_after = await agent_with_memory.agent_memory.get_recent_memories(\n            context, limit=10\n        )\n        assert len(memories_after) == 0\n\n    @pytest.mark.asyncio\n    async def test_delete_text_memory_success(\n        self, workflow_handler, agent_with_memory, test_user, test_conversation\n    ):\n        \"\"\"Test successfully deleting a text memory.\"\"\"\n        # Create a context and add a text memory\n        context = ToolContext(\n            user=test_user,\n            conversation_id=test_conversation.id,\n            request_id=str(uuid.uuid4()),\n            agent_memory=agent_with_memory.agent_memory,\n        )\n\n        text_memory = await agent_with_memory.agent_memory.save_text_memory(\n            content=\"Test text memory\",\n            context=context,\n        )\n\n        memory_id = text_memory.memory_id\n\n        # Delete the memory\n        result = await workflow_handler._delete_memory(\n            agent_with_memory, test_user, test_conversation, memory_id\n        )\n\n        assert result.should_skip_llm is True\n        content = result.components[0].rich_component.content\n        assert \"Memory Deleted\" in content\n\n        # Verify memory is gone\n        text_memories_after = (\n            await agent_with_memory.agent_memory.get_recent_text_memories(\n                context, limit=10\n            )\n        )\n        assert len(text_memories_after) == 0\n\n    @pytest.mark.asyncio\n    async def test_delete_command_parsing(\n        self,\n        workflow_handler,\n        agent_with_memory,\n        admin_test_user,\n        test_conversation,\n    ):\n        \"\"\"Test that /delete command is properly parsed (admin only).\"\"\"\n        # Add a memory first\n        context = ToolContext(\n            user=admin_test_user,\n            conversation_id=test_conversation.id,\n            request_id=str(uuid.uuid4()),\n            agent_memory=agent_with_memory.agent_memory,\n        )\n\n        await agent_with_memory.agent_memory.save_tool_usage(\n            question=\"Test\",\n            tool_name=\"test\",\n            args={},\n            context=context,\n            success=True,\n        )\n\n        memories = await agent_with_memory.agent_memory.get_recent_memories(\n            context, limit=1\n        )\n        memory_id = memories[0].memory_id\n\n        # Test command parsing\n        result = await workflow_handler.try_handle(\n            agent_with_memory,\n            admin_test_user,\n            test_conversation,\n            f\"/delete {memory_id}\",\n        )\n\n        assert result.should_skip_llm is True\n        content = result.components[0].rich_component.content\n        assert \"Memory Deleted\" in content or \"Memory Not Found\" in content\n\n\nclass TestWorkflowComponentStructure:\n    \"\"\"Test the structure of components returned by workflow.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_help_has_rich_component(\n        self, workflow_handler, agent_with_memory, test_user, test_conversation\n    ):\n        \"\"\"Test that help command returns properly structured components.\"\"\"\n        result = await workflow_handler.try_handle(\n            agent_with_memory, test_user, test_conversation, \"/help\"\n        )\n\n        assert len(result.components) > 0\n        for component in result.components:\n            assert hasattr(component, \"rich_component\")\n            assert hasattr(component, \"simple_component\")\n\n    @pytest.mark.asyncio\n    async def test_memories_cards_have_proper_structure(\n        self, workflow_handler, agent_with_memory, test_user, test_conversation\n    ):\n        \"\"\"Test that memory cards have proper structure.\"\"\"\n        # Add a memory\n        context = ToolContext(\n            user=test_user,\n            conversation_id=test_conversation.id,\n            request_id=str(uuid.uuid4()),\n            agent_memory=agent_with_memory.agent_memory,\n        )\n\n        await agent_with_memory.agent_memory.save_tool_usage(\n            question=\"Test\",\n            tool_name=\"test_tool\",\n            args={\"key\": \"value\"},\n            context=context,\n            success=True,\n        )\n\n        result = await workflow_handler._get_recent_memories(\n            agent_with_memory, test_user, test_conversation\n        )\n\n        # Find card components\n        card_found = False\n        for component in result.components:\n            if hasattr(component.rich_component, \"type\"):\n                from vanna.core.rich_component import ComponentType\n\n                if component.rich_component.type == ComponentType.CARD:\n                    card_found = True\n                    # Check card has required fields\n                    assert hasattr(component.rich_component, \"title\")\n                    assert hasattr(component.rich_component, \"content\")\n                    assert hasattr(component.rich_component, \"actions\")\n                    assert len(component.rich_component.actions) > 0\n\n        assert card_found, \"Should have at least one card component\"\n\n\nclass TestStarterUI:\n    \"\"\"Test the starter UI functionality.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_starter_ui_single_component(\n        self, workflow_handler, agent_with_memory, test_user, test_conversation\n    ):\n        \"\"\"Test that starter UI returns a single component.\"\"\"\n\n        # Mock tool registry with SQL tool\n        class MockToolRegistryWithSQL:\n            async def get_schemas(self, user):\n                from vanna.core.tool import ToolSchema\n\n                return [\n                    ToolSchema(name=\"run_sql\", description=\"Run SQL\", parameters={})\n                ]\n\n        agent_with_memory.tool_registry = MockToolRegistryWithSQL()\n\n        components = await workflow_handler.get_starter_ui(\n            agent_with_memory, test_user, test_conversation\n        )\n\n        assert components is not None\n        assert len(components) == 1, \"Starter UI should return exactly one component\"\n        assert hasattr(components[0], \"rich_component\")\n\n    @pytest.mark.asyncio\n    async def test_starter_ui_user_view(\n        self, workflow_handler, agent_with_memory, test_conversation\n    ):\n        \"\"\"Test that non-admin users get a simple welcome message via RichTextComponent.\"\"\"\n        # Create user without admin role\n        user = User(\n            id=\"test_user\", email=\"test@example.com\", group_memberships=[\"user\"]\n        )\n\n        # Mock tool registry with SQL tool\n        class MockToolRegistryWithSQL:\n            async def get_schemas(self, user):\n                from vanna.core.tool import ToolSchema\n\n                return [\n                    ToolSchema(name=\"run_sql\", description=\"Run SQL\", parameters={})\n                ]\n\n        agent_with_memory.tool_registry = MockToolRegistryWithSQL()\n\n        components = await workflow_handler.get_starter_ui(\n            agent_with_memory, user, test_conversation\n        )\n\n        assert len(components) == 1\n        rich_text = components[0].rich_component\n        assert rich_text.type == ComponentType.TEXT\n        assert \"Welcome to Vanna AI\" in rich_text.content\n        # User view should be simple and not mention memory management\n        assert \"Memory Management\" not in rich_text.content\n        assert \"Admin\" not in rich_text.content\n\n    @pytest.mark.asyncio\n    async def test_starter_ui_admin_view(\n        self, workflow_handler, agent_with_memory, test_conversation\n    ):\n        \"\"\"Test that admin users get setup status and memory management info.\"\"\"\n        # Create admin user\n        admin_user = User(\n            id=\"admin_user\", email=\"admin@example.com\", group_memberships=[\"admin\"]\n        )\n\n        # Mock tool registry with SQL and memory tools\n        class MockToolRegistryComplete:\n            async def get_schemas(self, user):\n                from vanna.core.tool import ToolSchema\n\n                return [\n                    ToolSchema(name=\"run_sql\", description=\"Run SQL\", parameters={}),\n                    ToolSchema(\n                        name=\"search_saved_correct_tool_uses\",\n                        description=\"Search\",\n                        parameters={},\n                    ),\n                    ToolSchema(\n                        name=\"save_question_tool_args\",\n                        description=\"Save\",\n                        parameters={},\n                    ),\n                ]\n\n        agent_with_memory.tool_registry = MockToolRegistryComplete()\n\n        components = await workflow_handler.get_starter_ui(\n            agent_with_memory, admin_user, test_conversation\n        )\n\n        assert len(components) == 1\n        card = components[0].rich_component\n        assert card.type == ComponentType.CARD\n        assert hasattr(card, \"title\")\n        assert \"Admin\" in card.title  # Should indicate admin status\n        assert \"🔒 Admin View\" in card.content  # Should show admin badge\n        assert \"Setup:\" in card.content\n        # Admin view should include memory management info\n        assert \"Memory Management\" in card.content\n        assert \"As an admin\" in card.content\n        # Should have View Memories button\n        assert hasattr(card, \"actions\")\n        assert any(\n            \"View Memories\" in action.get(\"label\", \"\") for action in card.actions\n        )\n\n    @pytest.mark.asyncio\n    async def test_starter_ui_admin_without_memory(\n        self, workflow_handler, agent_with_memory, test_conversation\n    ):\n        \"\"\"Test that admin users see setup status even without memory tools.\"\"\"\n        # Create admin user\n        admin_user = User(\n            id=\"admin_user\", email=\"admin@example.com\", group_memberships=[\"admin\"]\n        )\n\n        # Mock tool registry with only SQL\n        class MockToolRegistrySQL:\n            async def get_schemas(self, user):\n                from vanna.core.tool import ToolSchema\n\n                return [\n                    ToolSchema(name=\"run_sql\", description=\"Run SQL\", parameters={}),\n                ]\n\n        agent_with_memory.tool_registry = MockToolRegistrySQL()\n\n        components = await workflow_handler.get_starter_ui(\n            agent_with_memory, admin_user, test_conversation\n        )\n\n        assert len(components) == 1\n        card = components[0].rich_component\n        assert card.type == ComponentType.CARD\n        # Admin should see setup status showing incomplete memory setup\n        assert \"Admin\" in card.title\n        assert \"🔒 Admin View\" in card.content\n        assert \"Setup:\" in card.content\n        assert \"Memory ✗\" in card.content\n        # Should NOT have View Memories button since memory is not available\n        memory_button_exists = any(\n            \"View Memories\" in action.get(\"label\", \"\")\n            for action in (card.actions or [])\n        )\n        assert not memory_button_exists\n\n\nclass TestAdminOnlyCommands:\n    \"\"\"Test that admin-only commands are properly restricted.\"\"\"\n\n    @pytest.mark.asyncio\n    async def test_non_admin_cannot_access_status(\n        self, workflow_handler, agent_with_memory, test_conversation\n    ):\n        \"\"\"Test that non-admin users cannot access /status command.\"\"\"\n        # Create non-admin user\n        user = User(id=\"user\", email=\"user@example.com\", group_memberships=[\"user\"])\n\n        result = await workflow_handler.try_handle(\n            agent_with_memory, user, test_conversation, \"/status\"\n        )\n\n        assert result.should_skip_llm is True\n        assert len(result.components) == 1\n        rich_text = result.components[0].rich_component\n        assert \"Access Denied\" in rich_text.content\n        assert \"administrators\" in rich_text.content\n\n    @pytest.mark.asyncio\n    async def test_non_admin_cannot_access_memories(\n        self, workflow_handler, agent_with_memory, test_conversation\n    ):\n        \"\"\"Test that non-admin users cannot access /memories command.\"\"\"\n        # Create non-admin user\n        user = User(id=\"user\", email=\"user@example.com\", group_memberships=[\"user\"])\n\n        result = await workflow_handler.try_handle(\n            agent_with_memory, user, test_conversation, \"/memories\"\n        )\n\n        assert result.should_skip_llm is True\n        assert len(result.components) == 1\n        rich_text = result.components[0].rich_component\n        assert \"Access Denied\" in rich_text.content\n        assert \"administrators\" in rich_text.content\n\n    @pytest.mark.asyncio\n    async def test_non_admin_cannot_delete_memories(\n        self, workflow_handler, agent_with_memory, test_conversation\n    ):\n        \"\"\"Test that non-admin users cannot use /delete command.\"\"\"\n        # Create non-admin user\n        user = User(id=\"user\", email=\"user@example.com\", group_memberships=[\"user\"])\n\n        result = await workflow_handler.try_handle(\n            agent_with_memory, user, test_conversation, \"/delete some-memory-id\"\n        )\n\n        assert result.should_skip_llm is True\n        assert len(result.components) == 1\n        rich_text = result.components[0].rich_component\n        assert \"Access Denied\" in rich_text.content\n        assert \"administrators\" in rich_text.content\n\n    @pytest.mark.asyncio\n    async def test_admin_can_access_memories(\n        self, workflow_handler, agent_with_memory, test_conversation\n    ):\n        \"\"\"Test that admin users can access /memories command.\"\"\"\n        # Create admin user\n        admin_user = User(\n            id=\"admin\", email=\"admin@example.com\", group_memberships=[\"admin\"]\n        )\n\n        result = await workflow_handler.try_handle(\n            agent_with_memory, admin_user, test_conversation, \"/memories\"\n        )\n\n        assert result.should_skip_llm is True\n        # Should show memories (even if empty, no access denied)\n        assert len(result.components) >= 1\n        assert \"Access Denied\" not in result.components[0].rich_component.content\n\n    @pytest.mark.asyncio\n    async def test_help_shows_admin_commands_for_admin(\n        self, workflow_handler, agent_with_memory, test_conversation\n    ):\n        \"\"\"Test that /help shows admin commands for admin users.\"\"\"\n        # Create admin user\n        admin_user = User(\n            id=\"admin\", email=\"admin@example.com\", group_memberships=[\"admin\"]\n        )\n\n        result = await workflow_handler.try_handle(\n            agent_with_memory, admin_user, test_conversation, \"/help\"\n        )\n\n        assert result.should_skip_llm is True\n        content = result.components[0].rich_component.content\n        assert \"Admin Commands\" in content\n        assert \"/status\" in content\n        assert \"/memories\" in content\n        assert \"/delete\" in content\n\n    @pytest.mark.asyncio\n    async def test_help_hides_admin_commands_for_non_admin(\n        self, workflow_handler, agent_with_memory, test_conversation\n    ):\n        \"\"\"Test that /help hides admin commands for non-admin users.\"\"\"\n        # Create non-admin user\n        user = User(id=\"user\", email=\"user@example.com\", group_memberships=[\"user\"])\n\n        result = await workflow_handler.try_handle(\n            agent_with_memory, user, test_conversation, \"/help\"\n        )\n\n        assert result.should_skip_llm is True\n        content = result.components[0].rich_component.content\n        assert \"Admin Commands\" not in content\n        # Still shows regular commands\n        assert \"/help\" in content\n        # But not admin commands\n        assert \"/status\" not in content\n\n\nif __name__ == \"__main__\":\n    pytest.main([__file__, \"-v\"])\n"
  },
  {
    "path": "tox.ini",
    "content": "[tox]\nenvlist =\n    ruff\n    mypy\n    py311-unit\n    py311-agent-memory-sanity\n    py311-anthropic\n    py311-openai\n    py311-legacy\n    py311-chromadb\n    py311-qdrant\n    py311-faiss\n    py311-postgres-sanity\n    py311-sqlite-sanity\n    py311-snowflake-sanity\n    py311-mysql-sanity\n    py311-clickhouse-sanity\n    py311-oracle-sanity\n    py311-bigquery-sanity\n    py311-duckdb-sanity\n    py311-mssql-sanity\n    py311-presto-sanity\n    py311-hive-sanity\n\n[testenv]\ndeps =\n    pytest>=7.0.0\n    pytest-asyncio>=0.21.0\n\n[testenv:py311-unit]\ndescription = Run unit tests (no external dependencies required)\ncommands =\n    pytest tests/test_tool_permissions.py tests/test_llm_context_enhancer.py tests/test_workflow.py tests/test_memory_tools.py -v\n\n[testenv:py311-agent-memory-sanity]\ndescription = Run sanity tests for all AgentMemory implementations (no actual service connections required)\ndeps =\n    pytest>=7.0.0\n    pytest-asyncio>=0.21.0\nextras =\n    chromadb\n    qdrant\ncommands =\n    pytest tests/test_agent_memory_sanity.py -v\n\n[testenv:py311-anthropic]\ndescription = Test with Anthropic\nextras = anthropic\npassenv = ANTHROPIC_API_KEY\ncommands =\n    python -c \"from vanna.integrations.anthropic import AnthropicLlmService; print('✓ Anthropic import successful')\"\n    pytest tests/ -v -m anthropic\n\n[testenv:py311-openai]\ndescription = Test with OpenAI\nextras = openai\npassenv = OPENAI_API_KEY\ncommands =\n    python -c \"from vanna.integrations.openai import OpenAILlmService; print('✓ OpenAI import successful')\"\n    pytest tests/ -v -m openai\n\n; [testenv:py311-gemini]\n; description = Test with Google Gemini\n; extras = gemini\n; passenv =\n;     GOOGLE_API_KEY\n;     GEMINI_API_KEY\n; commands =\n;     python -c \"from vanna.integrations.google import GeminiLlmService; print('✓ Gemini import successful')\"\n;     pytest tests/ -v -m gemini\n\n; [testenv:py311-ollama]\n; description = Test with Ollama\n; extras = ollama\n; passenv = OLLAMA_HOST\n; commands =\n;     python -c \"from vanna.integrations.ollama import OllamaLlmService; print('✓ Ollama import successful')\"\n;     pytest tests/ -v -m ollama\n\n[testenv:py311-legacy]\ndescription = Test LegacyVannaAdapter with Anthropic\nextras =\n    anthropic\n    chromadb\npassenv = ANTHROPIC_API_KEY\ncommands =\n    python -c \"from vanna.legacy.adapter import LegacyVannaAdapter; from vanna.legacy.chromadb import ChromaDB_VectorStore; from vanna.legacy.mock import MockLLM; print('✓ Legacy adapter imports successful')\"\n    pytest tests/test_legacy_adapter.py -v -m legacy\n\n[testenv:py311-chromadb]\ndescription = Test ChromaDB AgentMemory\nextras = chromadb\ncommands =\n    pytest tests/test_agent_memory.py::TestLocalAgentMemory -k chromadb -v\n\n[testenv:py311-qdrant]\ndescription = Test Qdrant AgentMemory\nextras = qdrant\ncommands =\n    pytest tests/test_agent_memory.py::TestLocalAgentMemory -k qdrant -v\n\n[testenv:py311-faiss]\ndescription = Test FAISS AgentMemory\ndeps =\n    pytest>=7.0.0\n    pytest-asyncio>=0.21.0\n    faiss-cpu\ncommands =\n    pytest tests/test_agent_memory.py::TestLocalAgentMemory -k faiss -v\n\n[testenv:py311-db-sanity]\ndescription = Run sanity tests for all database implementations (no actual DB connections required)\ndeps =\n    pytest>=7.0.0\n    pytest-asyncio>=0.21.0\n    pytest-mock>=3.10.0\nextras =\n    postgres\ncommands =\n    pytest tests/test_database_sanity.py -v\n\n[testenv:py311-postgres-sanity]\ndescription = Sanity tests for PostgreSQL implementation\ndeps =\n    pytest>=7.0.0\n    pytest-asyncio>=0.21.0\nextras = postgres\ncommands =\n    python -c \"from vanna.integrations.postgres import PostgresRunner; print('✓ PostgresRunner import successful')\"\n    pytest tests/test_database_sanity.py::TestPostgresRunner -v\n\n[testenv:py311-sqlite-sanity]\ndescription = Sanity tests for SQLite implementation\ndeps =\n    pytest>=7.0.0\n    pytest-asyncio>=0.21.0\ncommands =\n    python -c \"from vanna.integrations.sqlite import SqliteRunner; print('✓ SqliteRunner import successful')\"\n    pytest tests/test_database_sanity.py::TestSqliteRunner -v\n\n[testenv:py311-snowflake-sanity]\ndescription = Sanity tests for Snowflake implementation\ndeps =\n    pytest>=7.0.0\n    pytest-asyncio>=0.21.0\nextras = snowflake\ncommands =\n    python -c \"from vanna.integrations.snowflake import SnowflakeRunner; print('✓ SnowflakeRunner import successful')\"\n    pytest tests/test_database_sanity.py::TestSnowflakeRunner -v\n\n[testenv:py311-mysql-sanity]\ndescription = Sanity tests for MySQL implementation\ndeps =\n    pytest>=7.0.0\n    pytest-asyncio>=0.21.0\nextras = mysql\ncommands =\n    python -c \"from vanna.integrations.mysql import MySQLRunner; print('✓ MySQLRunner import successful')\"\n    pytest tests/test_database_sanity.py::TestMySQLRunner -v\n\n[testenv:py311-clickhouse-sanity]\ndescription = Sanity tests for ClickHouse implementation\ndeps =\n    pytest>=7.0.0\n    pytest-asyncio>=0.21.0\nextras = clickhouse\ncommands =\n    python -c \"from vanna.integrations.clickhouse import ClickHouseRunner; print('✓ ClickHouseRunner import successful')\"\n    pytest tests/test_database_sanity.py::TestClickHouseRunner -v\n\n[testenv:py311-oracle-sanity]\ndescription = Sanity tests for Oracle implementation\ndeps =\n    pytest>=7.0.0\n    pytest-asyncio>=0.21.0\nextras = oracle\ncommands =\n    python -c \"from vanna.integrations.oracle import OracleRunner; print('✓ OracleRunner import successful')\"\n    pytest tests/test_database_sanity.py::TestOracleRunner -v\n\n[testenv:py311-bigquery-sanity]\ndescription = Sanity tests for BigQuery implementation\ndeps =\n    pytest>=7.0.0\n    pytest-asyncio>=0.21.0\nextras = bigquery\ncommands =\n    python -c \"from vanna.integrations.bigquery import BigQueryRunner; print('✓ BigQueryRunner import successful')\"\n    pytest tests/test_database_sanity.py::TestBigQueryRunner -v\n\n[testenv:py311-duckdb-sanity]\ndescription = Sanity tests for DuckDB implementation\ndeps =\n    pytest>=7.0.0\n    pytest-asyncio>=0.21.0\nextras = duckdb\ncommands =\n    python -c \"from vanna.integrations.duckdb import DuckDBRunner; print('✓ DuckDBRunner import successful')\"\n    pytest tests/test_database_sanity.py::TestDuckDBRunner -v\n\n[testenv:py311-mssql-sanity]\ndescription = Sanity tests for MSSQL implementation\ndeps =\n    pytest>=7.0.0\n    pytest-asyncio>=0.21.0\nextras = mssql\ncommands =\n    python -c \"from vanna.integrations.mssql import MSSQLRunner; print('✓ MSSQLRunner import successful')\"\n    pytest tests/test_database_sanity.py::TestMSSQLRunner -v\n\n[testenv:py311-presto-sanity]\ndescription = Sanity tests for Presto implementation\ndeps =\n    pytest>=7.0.0\n    pytest-asyncio>=0.21.0\nextras = presto\ncommands =\n    python -c \"from vanna.integrations.presto import PrestoRunner; print('✓ PrestoRunner import successful')\"\n    pytest tests/test_database_sanity.py::TestPrestoRunner -v\n\n[testenv:py311-hive-sanity]\ndescription = Sanity tests for Hive implementation\ndeps =\n    pytest>=7.0.0\n    pytest-asyncio>=0.21.0\nextras = hive\ncommands =\n    python -c \"from vanna.integrations.hive import HiveRunner; print('✓ HiveRunner import successful')\"\n    pytest tests/test_database_sanity.py::TestHiveRunner -v\n\n[testenv:ruff]\ndescription = Check code formatting and linting with ruff (uses pyproject.toml config)\nextras = dev\ncommands =\n    ruff format --check src/vanna/ tests/\n    ruff check src/vanna/ tests/\n\n[testenv:mypy]\ndescription = Run mypy type checking with strict mode\nextras = dev\ncommands =\n    mypy src/vanna/tools src/vanna/core src/vanna/capabilities src/vanna/agents src/vanna/utils src/vanna/web_components src/vanna/components --strict\n"
  }
]